在语言环境之间传输 arrays/classes/records

Transferring arrays/classes/records between locales

在典型的 N 体模拟中,在每个时期结束时,每个区域都需要将其自己的 world 部分(即所有物体)共享给其余部分语言环境。我正在使用本地视图方法(即使用 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。移动数据

现在,数组 wrapperswrapper 的每个实例都应该位于其语言环境中。具体来说,引用 (wrappers) 将位于 locale 0,但内部数据 (rca) 应位于相应的 locale。所以我们尝试将一些从 locale 1 移动到 locale 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),发送数组和记录的时间都是常数,这对我来说没有意义。我什至检查了 chplvisGET 的大小实际上增加了,但时间保持不变。

  2. 发送class字段的时间随着时间的推移而增加,这是有道理的,但是很慢,我不知道这里应该相信哪种情况。

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 生活在区域 1,尽管它显然应该在区域 2。我唯一的猜测是,在执行 .locale.id 时,数据(即记录的 .x )已经移动到查询区域设置 (1)。

所以总而言之,实验的第二部分导致了第二个问题,而没有回答第一部分。


注意:所有实验都是 运行 -nl 4chapel/chapel-gasnet docker 图像中。

很好的观察,让我看看是否可以阐明一些问题。

作为初始说明,使用 gasnet Docker 图像拍摄的任何时间都应持保留态度,因为该图像使用本地系统而不是 运行 模拟跨多个节点的执行每个语言环境都在其自己的计算节点上,如 Chapel 中所预期的那样。因此,它对开发分布式内存程序很有用,但性能特征可能与实际集群或超级计算机上的 运行 大不相同。也就是说,它仍然可以用于粗略计时(例如,您的“这需要更长的时间”观察)或使用 chplvisCommDiagnostics module.

计算通信

关于您对时间的观察,我还观察到数组-class 的情况要慢得多,我相信我可以解释其中的一些行为:

首先,重要的是要了解任何跨节点通信都可以使用 alpha + beta*length 这样的公式来表征。将 alpha 视为执行通信的基本成本,与长度无关。这表示通过软件堆栈向下调用以到达网络、将数据放在线路上、在另一端接收数据,然后通过软件堆栈将其备份到那里的应用程序的成本。 alpha 的精确值将取决于通信类型、软件堆栈的选择和物理硬件等因素。同时,将 beta 视为表示通信的每字节成本,正如您的直觉,更长的消息必然成本更高,因为有更多的数据要放在线上,或者可能要缓冲或复制,具体取决于如何沟通已落实

根据我的经验,对于大多数系统配置,alpha 的值通常支配 beta。这并不是说可以自由地进行更长的数据传输,而是执行时间的差异对于更长的传输和更短的传输往往比执行单个传输和许多传输要小得多。因此,在执行 n 个元素的一次传输与 n 个元素的传输之间进行选择时,您几乎总是想要前者。

为了调查您的计时,我将您的计时代码部分与对 CommDiagnostics 模块的调用括起来,如下所示:

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

并且发现,正如您对 chplvis 所做的那样,当我改变 max 时,本地化记录数组或整数数组所需的通信数量是恒定的,例如:

locale get execute_on
0 0 0
1 0 0
2 0 0
3 21 1

这与我对实现的期望是一致的:对于值类型数组,我们执行固定数量的通信来访问数组元数据,然后在单个数据传输以分摊管理费用(避免支付多笔 alpha 费用)。

相比之下,我发现定位classes数组的通信次数与数组大小成正比。例如,对于 max 的默认值 50,000,我看到:

locale get put execute_on
0 0 0 0
1 0 0 0
2 0 0 0
3 25040 25000 1

我认为这种区别的原因与 cowned classes 的数组有关,其中只有一个 class 变量可以一次“拥有”一个给定的 ctuff 对象。因此,当将数组 c 的元素从一个区域复制到另一个区域时,您不仅在复制原始数据(如记录和整数情况),而且还对每个元素执行所有权转移。这基本上需要在将其值复制到本地 class 变量后将远程值设置为 nil。在我们当前的实现中,这似乎是使用远程 get 将远程 class 值复制到本地,然后使用远程 put 将远程值设置为 nil,因此,我们对每个数组元素都有一个 get 和 put,导致 O(n) 通信而不是像前面的情况那样的 O(1)。通过额外的努力,我们可能会让编译器优化这种情况,但我相信由于需要执行所有权转移,它总是比其他情况更昂贵。

我测试了 owned classes 通过将 ctuff 对象从 owned 更改为 unmanaged 导致额外开销的假设,这从实现中删除了任何所有权语义。当我这样做时,我看到了恒定数量的通信,如在价值案例中:

locale get execute_on
0 0 0
1 0 0
2 0 0
3 21 1

我相信这代表了这样一个事实,即一旦语言不需要管理 class 变量的所有权,它就可以简单地再次在一次传输中传输它们的指针值。

除了这些性能说明之外,在选择使用哪一个时,了解 classes 和记录之间的关键语义差异也很重要。 class 对象分配在堆上,class 变量本质上是对该对象的引用或指针。因此,当 class 变量从一个语言环境复制到另一个语言环境时,只会复制指针,而原始对象仍保留在原处(无论好坏)。相反,记录变量代表对象本身,可以被认为是“就地”分配的(例如,在局部变量的堆栈上)。当记录变量从一个区域复制到另一个区域时,复制的是对象本身(即记录的字段值),从而产生对象本身的新副本。有关详细信息,请参阅

继续你的第二个观察,我相信你的解释是正确的,这可能是实现中的一个错误(我需要多考虑一下才能有信心)。具体来说,我认为你是正确的,正在评估 wrappers_ref.r[1].x1 ,结果存储在局部变量中,并且 .locale.id 查询被应用于存储结果而不是原始字段。我通过将 ref 带到现场然后打印该 ref 的 locale.id 来测试这个理论,如下所示:

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

这似乎给出了正确的结果。我还查看了生成的代码,这似乎表明我们的理论是正确的。我不认为实施应该以这种方式行事,但在有信心之前需要多考虑一下。如果您想在 Chapel's GitHub issues page 上针对此问题提出错误,以便在那里进行进一步讨论,我们将不胜感激。