3

我有一段 C 代码,如下所示:

for(int i = 0; i < numRows; i++) {
   double *myRow = matrixPtr + (i * numCols);
   for (int j = 0; j < numCols; j++) {
      someOperation(myRow[j]);
   }
}

matrixPtr以行优先布局存储的 2D 矩阵在哪里。获取每行的引用/指针是为了使代码更具可读性,并避免需要为最内层循环(即matrixPtr[(i*numCols)+j])中的每次访问计算行偏移量。

在 Chapel 中,如果我要翻译上面的代码,并尝试将其紧密匹配,我可能会得到这样的结果:

for i in 0..numRows-1 {
   ref myRow = matrix[i,..];
   for j in 0..numCols-1 {
      someOperation(myRow[j]);
   }
}

其中matrix是 Chapel 矩阵,myRow是对矩阵行切片的引用。

我注意到,与省略数组切片步骤以获取行引用并直接访问matrixby相比,上述 Chapel 代码的性能非常慢[i,j],我认为在编译器优化后与上面的 C 代码非常相似(实际上,其性能与上面的 C 代码几乎相同):

for i in 0..numRows-1 {
   for j in 0..numCols-1 {
      someOperation(matrix[i,j]);
   }
}

我可以理解为什么它会变慢,因为您需要执行数组切片来获取每一行。但我想知道的是为什么 Chapel 中的数组切片要慢得多?首先,我的教堂体验充其量是很少的。

我试图查看生成的 C 代码以更好地了解正在发生的事情。我首先认为每次创建行引用时构建有界范围都会产生很多开销。为了测试这一点,我预先创建了范围并使用了它(即ref myRow = matrix[i,colRange])。但是,这似乎并没有改善运行时间。生成的 C 代码中唯一可以作为潜在线索隔离的另一部分是更改数组等级(或类似内容)的函数。

从角度来看,这种类型的矩阵运算在我的一个应用程序中执行了很多次,相比之下numRows非常大和numCols非常小,使用数组切片时 Chapel 代码的性能比直接访问矩阵慢 30-40 倍with [i,j]--fast使用编译期间使用的标志)。

谢谢

更新:

这里有两个小程序,它们应该产生报告的减速(大约在 20 到 25 倍之间)并使用--fast标志编译。direct.chpl 程序产生的执行时间与获取矩阵的 cPtr 然后计算行偏移量的版本相同,如上面的帖子中所述。

切片.chpl

use Time;
var numRows = 100000;
var numCols = 35;
var D : domain(2) = {0..numRows-1, 0..numCols-1};
var mat : [D] real;
mat = 1.0;
var totalTimer : Timer;
var accum : [0..numCols-1] real;
accum = 0.0;

totalTimer.start();
for i in 0..numRows-1 {
    ref myRow = mat(i,..);
    for j in 0..numCols-1 {
        accum[j] += i * myRow[j];
    }
}

totalTimer.stop();
writeln("Accum:");
writeln(accum);
writeln("\nTotal Elapsed time: ", totalTimer.elapsed(), " seconds");

直接.chpl:

use Time;
var numRows = 100000;
var numCols = 35;
var D : domain(2) = {0..numRows-1, 0..numCols-1};
var mat : [D] real;
mat = 1.0;
var totalTimer : Timer;
var accum : [0..numCols-1] real;
accum = 0.0;

totalTimer.start();
for i in 0..numRows-1 {
    for j in 0..numCols-1 {
        accum[j] += i * mat[i,j];
    }
}

totalTimer.stop();
writeln("Accum:");
writeln(accum);
writeln("\nTotal Elapsed time: ", totalTimer.elapsed(), " seconds");

slices.chpl 的输出:

Accum:
4.99995e+09 4.99995e+09 4.99995e+09 4.99995e+09...

Total Elapsed time: 0.124494 seconds

direct.chpl 的输出:

Accum:
4.99995e+09 4.99995e+09 4.99995e+09 4.99995e+09...

Total Elapsed time: 0.005211 seconds

所以这似乎是运行时的 23 倍差异。这并不是我在实际应用程序中看到的 30-40 倍的差异,但肯定超出了我的预期。

4

1 回答 1

2

我想知道的是为什么 Chapel 中的数组切片比我的 C 代码慢得多?

我认为这里的答案是 Chapel 的数组切片旨在支持数组支持的所有操作,包括边界检查和查询、迭代、传递给采用数组参数的例程、重新索引、后续切片等。这种支持需要设置元数据来描述数组及其域(索引集)——在这种情况下,通过将matrix的域与{i, ..}. 因此,正如您正确预期的那样,从本质上讲,它比 C 代码中的指针数学工作更多。

也就是说,您报告的速度慢了 30-40 倍,这让我们感到惊讶。我和一位同事试图重现您的实验,发现开销更多的是 2x 球场(我的结果是在 Mac 笔记本电脑上收集的)。这使我们想知道您的情况可能会有所不同。我最好的猜测是,您的程序的某些内容正在跨越区域设置边界到达远程内存,例如通过对远程或分布式数组进行切片。无论如何,如果您能够共享重现此问题的代码,我建议针对此问题打开一个GitHub 问题,其中包含有关如何收集数据的说明,以支持进一步探索它。

一般而言,我们倾向于不鼓励在性能关键代码中使用数组视图(数组切片和重新索引),尤其不鼓励将其作为尝试重现标量 C 风格优化的手段。我们的偏好是让用户以更简洁/更直接的风格编写代码,并依靠 Chapel 编译器对其进行优化(和/或在您发现我们将性能留在地板上的情况时告诉我们)。Chapel 的数组视图主要被设计为一种生产力功能,例如支持创建数组或子数组的视图以满足现有例程的形式参数要求。

于 2018-01-16T00:51:00.740 回答