로케일 간 배열 / 클래스 / 레코드 전송

Dec 26 2020

일반적인 N-Body 시뮬레이션에서 각 에포크가 끝날 때 각 로케일은 나머지 로케일 과 세계 의 자체 부분 (즉, 모든 바디)을 공유해야 합니다. 나는 이것을 로컬 뷰 접근법 (즉, on Loc서술문 사용 )으로 작업하고있다. 이해할 수없는 이상한 행동을 만나서 좀 더 복잡한 테스트 프로그램을 만들기로 결정했습니다. 다음은 실험을 복제하는 코드입니다.

proc log(args...?n) {
    writeln("[locale = ", here.id, "] [", datetime.now(), "] => ", args);
}

const max: int = 50000;
record stuff {
    var x1: int;
    var x2: int;

    proc init() {
        this.x1 = here.id;
        this.x2 = here.id;
    }
}

class ctuff {
    var x1: int;
    var x2: int;

    proc init() {
        this.x1 = here.id;
        this.x2 = here.id;
    }
}

class wrapper {
    // The point is that total size (in bytes) of data in `r`, `c` and `a` are the same here, because the record and the class hold two ints per index.
    var r: [{1..max / 2}] stuff;
    var c: [{1..max / 2}] owned ctuff?;
    var a: [{1..max}] int;

    proc init() {
        this.a = here.id;
    }
}

proc test() {
    var wrappers: [LocaleSpace] owned wrapper?;
    coforall loc in LocaleSpace {
        on Locales[loc] {
            wrappers[loc] = new owned wrapper();
        }
    }

    // rest of the experiment further down.

}

여기서 두 가지 흥미로운 행동이 발생합니다.

1. 데이터 이동

이제 wrapperin array 의 각 인스턴스는 wrappers해당 로케일에 있어야합니다. 즉, 참조 ( wrappers) 로케일 0 살고되지만, 내부 데이터 ( r, c, a)의 각 장소에 거주한다. 따라서 다음과 같이 일부를 로케일 1에서 로케일 3으로 이동하려고합니다.

on Locales[3] {
    var timer: Timer;
    timer.start();
    var local_stuff = wrappers[1]!.r;
    timer.stop();
    log("get r from 1", timer.elapsed());
    log(local_stuff);
}

on Locales[3] {
    var timer: Timer;
    timer.start();
    var local_c = wrappers[1]!.c;
    timer.stop();
    log("get c from 1", timer.elapsed());
}

on Locales[3] {
    var timer: Timer;
    timer.start();
    var local_a = wrappers[1]!.a;
    timer.stop();
    log("get a from 1", timer.elapsed());
}

놀랍게도 내 타이밍은

  1. 크기 ( const max)에 관계없이 배열 및 레코드를 보내는 시간이 일정하지 않습니다. 으로 확인 했는데 실제로 chplvis크기가 GET증가하지만 시간은 동일합니다.

  2. 클래스 필드를 보내는 데 걸리는 시간은 시간이 지남에 따라 증가합니다. 이는 말이 되겠지만 상당히 느리고 여기에서 어떤 경우를 신뢰할지 모르겠습니다.

2. 로케일을 직접 쿼리합니다.

문제를 이해하기 위해 .locale.id일부 변수를 직접 쿼리합니다 . 먼저 로케일 2에서 로케일 2에있을 것으로 예상되는 데이터를 쿼리합니다.

on Locales[2] {
    var wrappers_ref = wrappers[2]!; // This is always 1 GET from 0, okay.
    log("array",
        wrappers_ref.a.locale.id,
        wrappers_ref.a[1].locale.id
    );
    log("record",
        wrappers_ref.r.locale.id,
        wrappers_ref.r[1].locale.id,
        wrappers_ref.r[1].x1.locale.id,
    );
    log("class",
        wrappers_ref.c.locale.id,
        wrappers_ref.c[1]!.locale.id,
        wrappers_ref.c[1]!.x1.locale.id
    );
}

결과는 다음과 같습니다.

[locale = 2] [2020-12-26T19:36:26.834472] => (array, 2, 2)
[locale = 2] [2020-12-26T19:36:26.894779] => (record, 2, 2, 2)
[locale = 2] [2020-12-26T19:36:27.023112] => (class, 2, 2, 2)

어느 것이 예상됩니다. 그러나 로케일 1에서 동일한 데이터의 로케일을 쿼리하면 다음을 얻습니다.

[locale = 1] [2020-12-26T19:34:28.509624] => (array, 2, 2)
[locale = 1] [2020-12-26T19:34:28.574125] => (record, 2, 2, 1)
[locale = 1] [2020-12-26T19:34:28.700481] => (class, 2, 2, 2)

wrappers_ref.r[1].x1.locale.id분명히 로케일 2에 있어야하지만 로케일 1에 있음을 암시합니다 . 내 유일한 추측은 시간 .locale.id이 실행될 때까지 데이터 (즉 .x, 레코드)가 이미 쿼리 로케일 (1)로 이동 되었다는 것 입니다.

그래서 대체로 실험의 두 번째 부분은 두 번째 질문으로 이어지고 첫 번째 부분에는 대답하지 않습니다.


참고 : 모든 실험은 도커 이미지 -nl 4에서 실행됩니다 chapel/chapel-gasnet.

답변

4 Brad Dec 27 2020 at 23:49

좋은 관찰입니다. 제가 빛을 비출 수 있는지 봅시다.

초기 참고로, 가스 넷 Docker 이미지로 찍은 타이밍은 Chapel에서 의도 한대로 자체 컴퓨팅 노드에서 각 로케일을 실행하는 대신 로컬 시스템을 사용하여 여러 노드에서 실행을 시뮬레이션하기 때문에 소금을 사용하여 촬영해야합니다. 결과적으로 분산 메모리 프로그램을 개발하는 데 유용하지만 성능 특성은 실제 클러스터 또는 슈퍼 컴퓨터에서 실행하는 것과 매우 다를 수 있습니다. 즉, 여전히 (예를 들어, 당신이 관찰 "이 훨씬 더 긴 시간이 소요됩니다") 거친 타이밍을 얻기 위해 또는 사용하여 계산 통신에 유용 할 수 있습니다 말했다 chplvis또는 CommDiagnostics 모듈 .

타이밍에 대한 당신의 관찰과 관련하여, 나는 또한 클래스 배열의 경우가 훨씬 느리다는 것을 관찰하고 나는 몇 가지 행동을 설명 할 수 있다고 믿습니다.

첫째, 모든 노드 간 통신은 .NET과 같은 공식을 사용하여 특성화 될 수 있음을 이해하는 것이 중요합니다 alpha + beta*length. alpha길이에 관계없이 통신을 수행하는 기본 비용을 나타내는 것으로 생각하십시오 . 이것은 네트워크에 도달하기 위해 소프트웨어 스택을 통해 호출하고, 데이터를 유선에 배치하고, 다른 쪽에서 수신하고, 소프트웨어 스택을 통해 응용 프로그램으로 백업하는 비용을 나타냅니다. 알파의 정확한 값은 통신 유형, 소프트웨어 스택 선택 및 물리적 하드웨어와 같은 요소에 따라 달라집니다. 한편, beta직감적으로 더 긴 메시지는 통신이 구현되는 방법에 따라 더 많은 데이터를 연결하거나 잠재적으로 버퍼링 또는 복사해야하기 때문에 더 많은 비용이 드는 통신의 바이트 당 비용을 나타내는 것으로 생각 하십시오.

제 경험상 대부분의 시스템 구성 에서의 가치는 alpha일반적으로 지배적 beta입니다. 더 긴 데이터 전송을 무료로 수행 할 수 있다는 말은 아니지만 실행 시간의 차이는 단일 전송을 수행하는 것보다 긴 전송과 짧은 전송을 비교하는 것보다 훨씬 더 작은 경향이 있습니다. 결과적으로 하나의 n요소 전송과 하나의 요소 전송 중에서 선택할 때 n거의 항상 전자를 원할 것입니다.

타이밍을 조사하기 위해 CommDiagnostics다음과 같이 모듈 호출로 시간 제한 코드 부분을 묶었습니다 .

resetCommDiagnostics();
startCommDiagnostics();
...code to time here...
stopCommDiagnostics();
printCommDiagnosticsTable();

에서했던 것처럼 chplvis레코드 배열 또는 int 배열을 지역화하는 데 필요한 통신 수는 내가 변경함에 따라 일정하다는 것을 발견 max했습니다. 예를 들면 다음과 같습니다.

장소 가져 오기 execute_on
0 0 0
1 0 0
2 0 0
21 1

이것은 구현에서 기대했던 것과 일치합니다. 값 유형 배열의 경우 배열 메타 데이터에 액세스하기 위해 고정 된 수의 통신을 수행 한 다음 단일 데이터 전송으로 배열 요소 자체를 통신하여 간접비 (여러 alpha비용을 지불하지 마십시오 ).

반대로 클래스 배열을 지역화하기위한 통신 수는 배열 크기에 비례한다는 것을 알았습니다. 예를 들어의 기본값 인 50,000에 대해 max다음을 확인했습니다.

장소 가져 오기 놓다 execute_on
0 0 0 0
1 0 0 0
2 0 0 0
25040 25000 1

나는이 구별의 이유 cowned클래스 의 배열 이라는 사실과 관련이 있다고 믿습니다. 클래스 의 배열은 한 번에 하나의 클래스 변수 만이 주어진 ctuff객체를 "소유"할 수 있습니다 . 결과적으로 c한 로케일에서 다른 로케일로 배열의 요소를 복사 할 때 레코드 및 정수 케이스와 같이 원시 데이터를 복사하는 것이 아니라 요소별로 소유권 이전을 수행합니다. 이것은 본질적으로 nil로컬 클래스 변수에 값을 복사 한 후 원격 값을로 설정해야합니다 . 현재 구현에서는 get원격 클래스 값을 로컬 클래스 값으로 복사하기 위해 원격 put을 사용하고 원격 값을로 설정 하는 원격 을 사용하여 수행되는 것으로 보입니다 nil. 따라서 배열 요소 당 가져 오기 및 배치가 있으므로 결과는 O입니다. (n) 이전 사례에서와 같이 O (1)보다는 통신. 추가적인 노력으로 우리는 잠재적으로 컴파일러가이 경우를 최적화하도록 할 수 있지만, 소유권 이전을 수행해야하기 때문에 항상 다른 것보다 더 비쌀 것이라고 생각합니다.

그 가설 테스트 owned클래스가 변경하여 추가 오버 헤드가 발생했다 ctuff되는 것을 객체 ownedunmanaged구현에서 어떠한 소유권 의미를 제거하는을. 이렇게하면 가치 사례에서와 같이 일정한 수의 커뮤니케이션이 표시됩니다.

장소 가져 오기 execute_on
0 0 0
1 0 0
2 0 0
21 1

나는 이것이 언어가 클래스 변수의 소유권을 관리 할 필요가 없게되면 포인터 값을 한번의 전송으로 다시 전송할 수 있다는 사실을 나타낸다고 생각합니다.

이러한 성능 참고 사항 외에도 사용할 항목을 선택할 때 클래스와 레코드 간의 주요 의미 차이를 이해하는 것이 중요합니다. 클래스 개체는 힙에 할당되고 클래스 변수는 기본적으로 해당 개체에 대한 참조 또는 포인터입니다. 따라서 클래스 변수가 한 로케일에서 다른 로케일로 복사 될 때 포인터 만 복사되고 원래 객체는 원래 위치에 남아 있습니다 (좋든 나쁘 든간에). 대조적으로, 레코드 변수는 객체 자체를 나타내며 "제자리"(예 : 지역 변수의 스택에서) 할당 된 것으로 생각할 수 있습니다. 레코드 변수가 한 로케일에서 다른 로케일로 복사 될 때 복사되는 것은 객체 자체 (즉, 레코드의 필드 값)이며, 그 결과 객체 자체의 새로운 사본이 생성됩니다. 자세한 내용은 이 SO 질문 을 참조 하십시오.

두 번째 관찰로 넘어 가면 귀하의 해석이 정확하고 이것이 구현의 버그 일 수 있다고 믿습니다 (자신감을 갖기 위해 조금 더 꼼꼼히 살펴볼 필요가 있습니다). 특히, wrappers_ref.r[1].x1결과가 .locale.id지역 변수에 저장되고 쿼리가 원래 필드가 아닌 결과를 저장하는 지역 변수에 적용되는 상황이 발생하는 것이 옳다고 생각 합니다. 나는 ref필드에 a 를 가져간 다음 locale.id그 심판을 다음과 같이 인쇄 하여이 이론을 테스트했습니다 .

ref x1loc = wrappers_ref.r[1].x1;
...wrappers_ref.c[1]!.x1.locale.id...

그리고 그것은 올바른 결과를주는 것 같았습니다. 또한 우리 이론이 옳다는 것을 나타내는 생성 된 코드를 살펴 보았습니다. 구현이 이런 식으로 작동해야한다고 생각하지는 않지만 자신감을 갖기 전에 조금 더 생각할 필요가 있습니다. Chapel의 GitHub 문제 페이지 에서 이에 대한 버그를 열고 싶다면 거기에 대한 추가 논의를 위해 감사하겠습니다.