根据Chapel的可用文档,(整个)数组 像这样的陈述
A = B + alpha * C; // with A, B, and C being arrays, and alpha some scalar
在语言中实现为以下迭代:
forall (a,b,c) in zip(A,B,C) do
a = b + alpha * c;
因此,数组语句默认由一组并行执行 线程。不幸的是,这似乎也完全排除了 (部分或全部)此类语句的矢量化。这 对于习惯于 Fortran 或 Python/Numpy 等语言的程序员来说,可能会带来性能惊喜(其中的默认行为通常是仅矢量化数组语句)。
对于使用(全)数组语句的代码,该语句具有小到的数组 中等大小,矢量化损失(由 Linux 硬件确认) 性能计数器)以及 并行线程(不适合有效利用 此类问题中可用的细粒度数据并行性)可能会导致 性能严重下降。例如,考虑 以下版本的雅可比迭代都解决了相同的问题 在包含 300 x 300 区域的域上:
Jacobi_1
使用数组语句,如下所示:
/*
* Jacobi_1
*
* This program (adapted from the Chapel distribution) performs
* niter iterations of the Jacobi method for the Laplace equation
* using (whole-)array statements.
*
*/
config var n = 300; // size of n x n grid
config var niter = 10000; // number of iterations to perform
proc main() {
const Domain = {0..n+1,0..n+1}; // domain including boundary points
var iteration = 0; // iteration counter
var X, XNew: [Domain] real = 0.0; // declare arrays:
// X stores approximate solution
// XNew stores the next solution
X[n+1,1..n] = 1.0; // Set south boundary values to 1.0
do {
// compute next approximation
XNew[1..n,1..n] =
( X[0..n-1,1..n] + X[2..n+1,1..n] +
X[1..n,2..n+1] + X[1..n,0..n-1] ) / 4.0;
// update X with next approximation
X[1..n,1..n] = XNew[1..n,1..n];
// advance iteration counter
iteration += 1;
} while (iteration < niter);
writeln("Jacobi computation complete.");
writeln("# of iterations: ", iteration);
} // main
Jacobi_2
在整个过程中使用串行for循环(即仅(自动)矢量化 允许后端 C 编译器):
/*
* Jacobi_2
*
* This program (adapted from the Chapel distribution) performs
* niter iterations of the Jacobi method for the Laplace equation
* using (serial) for-loops.
*
*/
config var n = 300; // size of n x n grid
config var niter = 10000; // number of iterations to perform
proc main() {
const Domain = {0..n+1,0..n+1}; // domain including boundary points
var iteration = 0; // iteration counter
var X, XNew: [Domain] real = 0.0; // declare arrays:
// X stores approximate solution
// XNew stores the next solution
for j in 1..n do
X[n+1,j] = 1.0; // Set south boundary values to 1.0
do {
// compute next approximation
for i in 1..n do
for j in 1..n do
XNew[i,j] = ( X[i-1,j] + X[i+1,j] +
X[i,j+1] + X[i,j-1] ) / 4.0;
// update X with next approximation
for i in 1..n do
for j in 1..n do
X[i,j] = XNew[i,j];
// advance iteration counter
iteration += 1;
} while (iteration < niter);
writeln("Jacobi computation complete.");
writeln("# of iterations: ", iteration);
} // main
最后,Jacobi_3
最里面的循环被矢量化,只有 最外层的螺纹循环:
/*
* Jacobi_3
*
* This program (adapted from the Chapel distribution) performs
* niter iterations of the Jacobi method for the Laplace equation
* using both parallel and serial (vectorized) loops.
*
*/
config var n = 300; // size of n x n grid
config var niter = 10000; // number of iterations to perform
proc main() {
const Domain = {0..n+1,0..n+1}; // domain including boundary points
var iteration = 0; // iteration counter
var X, XNew: [Domain] real = 0.0; // declare arrays:
// X stores approximate solution
// XNew stores the next solution
for j in vectorizeOnly(1..n) do
X[n+1,j] = 1.0; // Set south boundary values to 1.0
do {
// compute next approximation
forall i in 1..n do
for j in vectorizeOnly(1..n) do
XNew[i,j] = ( X[i-1,j] + X[i+1,j] +
X[i,j+1] + X[i,j-1] ) / 4.0;
// update X with next approximation
forall i in 1..n do
for j in vectorizeOnly(1..n) do
X[i,j] = XNew[i,j];
// advance iteration counter
iteration += 1;
} while (iteration < niter);
writeln("Jacobi computation complete.");
writeln("# of iterations: ", iteration);
} // main
在具有 2 个处理器内核并使用两个处理器内核的笔记本电脑上运行这些代码 并行线程,人们发现Jacobi_1
是(令人惊讶的) 比Jacobi_2
慢十倍以上,这本身是(意料之中的) ~比Jacobi_3
慢1.6倍。
不幸的是,此默认行为使数组语句完全 对我的用例没有吸引力,即使对于会受益的算法也没有吸引力 极大地来自更简洁的符号和可读性 (全)数组语句可以提供。
教堂中的用户有没有办法更改此默认行为? 也就是说,用户可以自定义整个阵列的默认并行化吗 语句的方式是,在Jacobi_1
中使用的数组语句将 行为类似于Jacobi_2
中的代码(这对于代码开发和调试目的很有用),还是Jacobi_3
中的代码(在这三者中,这将是生产计算的首选方法)?
我试图通过将对"vectorizeOnly()
"的调用插入来实现这一点 上述"域"的定义,但无济于事。
Chapel 的目的是在用于实现forall
循环的每任务串行循环中自动支持矢量化(对于合法可矢量化的情况)。 然而,正如您所注意到的,该功能目前没有得到很好的支持(即使是您正在使用的vectorizeOnly()
迭代器也仅被视为原型)。
我会提到,在使用Chapel的LLVM后端时,我们往往会看到比使用(默认)C后端更好的矢量化结果,并且在使用Simon Moll的基于LLVM的区域矢量化器(萨尔大学)时,我们已经看到了更好的结果。 但我们也看到了LLVM后端性能低于C后端的情况,因此您的里程可能会有所不同。 但是,如果您关心矢量化,那么值得一试。
对于您的具体问题:
教堂中的用户有没有办法更改此默认行为?
有。 对于显式forall
循环,您可以编写自己的并行迭代器,该迭代器可用于为forall
循环指定不同于默认迭代器使用的实现策略。 如果你实现了你喜欢的一个,那么你可以编写(或克隆和修改)一个域映射(背景在这里)来控制默认情况下如何实现给定数组上的循环(即,如果没有显式调用迭代器)。 这允许最终用户为 Chapel 数组指定与我们默认支持的策略不同的实施策略。
关于您的三个代码变体,我注意到第一个使用多维拉链,目前已知存在严重的性能问题。 这可能是它与其他性能差异的主要原因。 例如,我怀疑如果您使用表单forall (i,j) in Domain ...
重写它,然后使用每个维度的 +/-1 索引,您会看到显着的改进(而且,我猜,性能与第三种情况更具可比性)。
对于第三个,我很好奇你看到的好处是由于矢量化还是仅仅由于多任务处理,因为你已经避免了第一个的性能问题和第二个的串行实现。 例如,您是否检查过使用 vectorizeOnly() 迭代器是否在没有该迭代器的情况下对相同代码增加了任何性能改进(或者在二进制文件上使用工具来检查是否正在发生矢量化?
在任何教堂性能研究中,请确保抛出--fast
编译器标志。 同样,为了获得最佳矢量化结果,您可以尝试 LLVM 后端。