调整记录数组大小的域挂起
Domain resizing on an array of records hangs
我有一个包含两个数组的超图数据结构,一个用于边,一个用于顶点(类似于二分图)。我在调整数组大小时遇到问题,所以我尝试了简化示例:
ar dom = {0..0};
var arr: [dom] int;
writeln(arr);
dom = {0..#2};
writeln(arr);
dom = {0..#1};
writeln(arr);
record Vertex {}
record Edge {}
record Wrapper {
type nodeType;
type idType;
var id: idType;
}
record NodeData {
type nodeIdType;
var ndom = {0..-1};
var neighborList: [ndom] nodeIdType;
proc numNeighbors() return ndom.numIndices;
var lock$: sync bool = true;
// This method is not parallel-safe
proc addNodes(vals) {
lock$; // acquire lock
neighborList.push_back(vals);
lock$ = true; // release the lock
}
proc readWriteThis(f) {
f <~> new ioLiteral("{ ndom = ") <~> ndom <~> new ioLiteral(", neighborlist = ") <~> neighborList <~> new ioLiteral(", lock$ = ") <~> lock$.readFF() <~> new ioLiteral(" }");
}
}
type vType = Wrapper(Vertex, int);
type eType = Wrapper(Edge, int);
var dom1 = {0..0};
var arr1: [dom1] NodeData(eType);
writeln(arr1);
dom1 = {0..#2};
writeln(arr1);
dom1 = {0..#1};
writeln(arr1);
当我尝试 运行 这段代码时,它挂起并显示以下输出:
$ ./resize -nl 1
salloc: Granted job allocation 15015
0
0 0
0
{ ndom = {0..-1}, neighborlist = , lock$ = true }
因此调整整数数组的大小非常好,但我无法调整记录数组的大小。我做错了什么?
附带说明一下,当我尝试在我的完整代码中调整域的大小时,我看到域发生了变化,但使用这些域的数组根本没有改变。至少代码不会挂起。
编辑
我尝试了另一个实际上更好地说明我原来问题的例子:
class Test {
var dom;
var ints: [dom] int;
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
这是我看到的输出:
$ ./resize -nl 1
salloc: Granted job allocation 15038
{dom = {0..-1}, ints = }
{dom = {0..1}, ints = }
salloc: Relinquishing job allocation 15038
所以我的问题是resize
方法没用。它确实会更改域,但不会更改成员数组。
以下是对您在编辑示例中遇到的问题的回应:
从 1.17 版开始,恐怕您陷入了编译器的黑暗角落,我很遗憾它的存在,但我认为我们可以让您摆脱困境。
从一些背景和重要的上下文开始:从一开始,Chapel 就支持 classes 和记录的构造函数(例如,proc C(...)
用于 class C
),但这些是他们的设计很天真,尤其是 w.r.t。通用 classes 和记录。在过去的几个版本中,我们一直在从构造器转向初始化器(例如,proc init(..)
对应 class C
)来解决这些限制。
从今天发布的 1.17 版开始,初始化器的状态相当好(例如,我现在将它们用于我编写的所有新代码并且很少发誓),但是如果您既不提供初始化器也不提供构造器 (如您的示例中所示),编译器将创建默认的 constructor(而不是默认的初始值设定项),因此 运行 可能会遇到其中一些 long-standing 问题。对于 1.18 版,目标是让编译器默认创建初始化程序并完全弃用构造函数。
因此,这里有一些方法可以解决 EDIT 中较小的测试程序的问题,在 Chapel 1.17 版中,所有这些似乎都为我产生了正确的输出:
1) 使 class 不那么通用。在这里,我给 dom
字段一个初始值,以便编译器可以确定它的类型,这显然有助于它使用默认构造函数生成预期的输出:
class Test {
var dom = {0..-1};
var ints: [dom] int;
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
2) 编写显式初始化程序。在这里,我将保留 dom
泛型,但会创建一个初始化程序来分配它以匹配您的 new
调用的签名:
class Test {
var dom;
var ints: [dom] int;
proc init(dom: domain(1)) {
this.dom = dom;
}
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
3)(不得已)请求编译器为你创建一个默认初始化器(而不是默认构造函数)。这种方法确实不适用于 end-users,并非适用于所有情况,并且将来会消失,但同时了解它可能会很方便。在这里,我将附注附加到 class 以告诉编译器创建默认初始化程序而不是默认构造函数。尽管默认情况下编译器不会创建默认初始化程序,但对于许多 classes 和记录,如果您要求它,它就可以创建,这恰好是其中之一:
pragma "use default init"
class Test {
var dom;
var ints: [dom] int;
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
为了 space,我在这里只解决了你的较短示例,而不是你的较长示例,但希望这些技术也能帮助它(并且很乐意花更多时间如果需要,可以使用更长的那个)。
以下是对您在原始示例中报告的问题的回应(与您的 EDIT 示例中的问题不同,单独回答):
您的原始代码失败的原因是 Chapel 为记录创建的默认分配。具体来说,给定一条记录 R
:
record R {
var x: t;
var y: t2;
var z: t3;
}
如果您没有在 R
之间创建赋值,编译器将创建一种通用形式:
proc =(ref lhs: R, rhs: R) {
lhs.x = rhs.x;
lhs.y = rhs.y;
lhs.z = rhs.z;
}
在您的示例中,这会导致问题,因为当前(从 Chapel 1.17.0 开始)重新分配数组时,其元素首先是默认值 constructed/initialized,然后再分配。因此,同步的 lock$
字段默认初始化为 true
,然后在复制数组元素时,赋值运算符尝试 re-assign 该字段,但它已经满了。
一个解决方案似乎允许您的代码编译并且 运行 对我来说是实现您自己的赋值函数,这样它就不会触及 lock$
字段:
proc =(ref lhs: NodeData(?t), rhs: NodeData(t)) {
lhs.ndom = rhs.ndom;
lhs.neighborList = rhs.neighborList;
// let's not touch lhs.lock$
}
但是您显然应该考虑这是否是适合您的用例的正确锁定规则。如果您认为此处需要更改某些内容 w.r.t。 initialization/assignment 语义 w.r.t。数组大小调整,请随意提交一个 Chapel GitHub issue against it.
我有一个包含两个数组的超图数据结构,一个用于边,一个用于顶点(类似于二分图)。我在调整数组大小时遇到问题,所以我尝试了简化示例:
ar dom = {0..0};
var arr: [dom] int;
writeln(arr);
dom = {0..#2};
writeln(arr);
dom = {0..#1};
writeln(arr);
record Vertex {}
record Edge {}
record Wrapper {
type nodeType;
type idType;
var id: idType;
}
record NodeData {
type nodeIdType;
var ndom = {0..-1};
var neighborList: [ndom] nodeIdType;
proc numNeighbors() return ndom.numIndices;
var lock$: sync bool = true;
// This method is not parallel-safe
proc addNodes(vals) {
lock$; // acquire lock
neighborList.push_back(vals);
lock$ = true; // release the lock
}
proc readWriteThis(f) {
f <~> new ioLiteral("{ ndom = ") <~> ndom <~> new ioLiteral(", neighborlist = ") <~> neighborList <~> new ioLiteral(", lock$ = ") <~> lock$.readFF() <~> new ioLiteral(" }");
}
}
type vType = Wrapper(Vertex, int);
type eType = Wrapper(Edge, int);
var dom1 = {0..0};
var arr1: [dom1] NodeData(eType);
writeln(arr1);
dom1 = {0..#2};
writeln(arr1);
dom1 = {0..#1};
writeln(arr1);
当我尝试 运行 这段代码时,它挂起并显示以下输出:
$ ./resize -nl 1
salloc: Granted job allocation 15015
0
0 0
0
{ ndom = {0..-1}, neighborlist = , lock$ = true }
因此调整整数数组的大小非常好,但我无法调整记录数组的大小。我做错了什么?
附带说明一下,当我尝试在我的完整代码中调整域的大小时,我看到域发生了变化,但使用这些域的数组根本没有改变。至少代码不会挂起。
编辑
我尝试了另一个实际上更好地说明我原来问题的例子:
class Test {
var dom;
var ints: [dom] int;
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
这是我看到的输出:
$ ./resize -nl 1
salloc: Granted job allocation 15038
{dom = {0..-1}, ints = }
{dom = {0..1}, ints = }
salloc: Relinquishing job allocation 15038
所以我的问题是resize
方法没用。它确实会更改域,但不会更改成员数组。
以下是对您在编辑示例中遇到的问题的回应:
从 1.17 版开始,恐怕您陷入了编译器的黑暗角落,我很遗憾它的存在,但我认为我们可以让您摆脱困境。
从一些背景和重要的上下文开始:从一开始,Chapel 就支持 classes 和记录的构造函数(例如,proc C(...)
用于 class C
),但这些是他们的设计很天真,尤其是 w.r.t。通用 classes 和记录。在过去的几个版本中,我们一直在从构造器转向初始化器(例如,proc init(..)
对应 class C
)来解决这些限制。
从今天发布的 1.17 版开始,初始化器的状态相当好(例如,我现在将它们用于我编写的所有新代码并且很少发誓),但是如果您既不提供初始化器也不提供构造器 (如您的示例中所示),编译器将创建默认的 constructor(而不是默认的初始值设定项),因此 运行 可能会遇到其中一些 long-standing 问题。对于 1.18 版,目标是让编译器默认创建初始化程序并完全弃用构造函数。
因此,这里有一些方法可以解决 EDIT 中较小的测试程序的问题,在 Chapel 1.17 版中,所有这些似乎都为我产生了正确的输出:
1) 使 class 不那么通用。在这里,我给 dom
字段一个初始值,以便编译器可以确定它的类型,这显然有助于它使用默认构造函数生成预期的输出:
class Test {
var dom = {0..-1};
var ints: [dom] int;
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
2) 编写显式初始化程序。在这里,我将保留 dom
泛型,但会创建一个初始化程序来分配它以匹配您的 new
调用的签名:
class Test {
var dom;
var ints: [dom] int;
proc init(dom: domain(1)) {
this.dom = dom;
}
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
3)(不得已)请求编译器为你创建一个默认初始化器(而不是默认构造函数)。这种方法确实不适用于 end-users,并非适用于所有情况,并且将来会消失,但同时了解它可能会很方便。在这里,我将附注附加到 class 以告诉编译器创建默认初始化程序而不是默认构造函数。尽管默认情况下编译器不会创建默认初始化程序,但对于许多 classes 和记录,如果您要求它,它就可以创建,这恰好是其中之一:
pragma "use default init"
class Test {
var dom;
var ints: [dom] int;
proc resize(size) {
dom = {dom.low..size};
}
}
var test = new Test(dom = {0..-1});
writeln(test);
test.resize(1);
writeln(test);
为了 space,我在这里只解决了你的较短示例,而不是你的较长示例,但希望这些技术也能帮助它(并且很乐意花更多时间如果需要,可以使用更长的那个)。
以下是对您在原始示例中报告的问题的回应(与您的 EDIT 示例中的问题不同,单独回答):
您的原始代码失败的原因是 Chapel 为记录创建的默认分配。具体来说,给定一条记录 R
:
record R {
var x: t;
var y: t2;
var z: t3;
}
如果您没有在 R
之间创建赋值,编译器将创建一种通用形式:
proc =(ref lhs: R, rhs: R) {
lhs.x = rhs.x;
lhs.y = rhs.y;
lhs.z = rhs.z;
}
在您的示例中,这会导致问题,因为当前(从 Chapel 1.17.0 开始)重新分配数组时,其元素首先是默认值 constructed/initialized,然后再分配。因此,同步的 lock$
字段默认初始化为 true
,然后在复制数组元素时,赋值运算符尝试 re-assign 该字段,但它已经满了。
一个解决方案似乎允许您的代码编译并且 运行 对我来说是实现您自己的赋值函数,这样它就不会触及 lock$
字段:
proc =(ref lhs: NodeData(?t), rhs: NodeData(t)) {
lhs.ndom = rhs.ndom;
lhs.neighborList = rhs.neighborList;
// let's not touch lhs.lock$
}
但是您显然应该考虑这是否是适合您的用例的正确锁定规则。如果您认为此处需要更改某些内容 w.r.t。 initialization/assignment 语义 w.r.t。数组大小调整,请随意提交一个 Chapel GitHub issue against it.