我正在用c ++开发一个2D数值模型,我想加快一个特定的成员函数,这会降低我的代码速度。该函数需要遍历模型中的每个i,j
网格点,然后在l
和m
上的每个网格点执行双重求和。功能如下:
int Class::Function(void) {
double loadingEta;
int i,j,l,m;
//etaLatLen=64, etaLonLen=2*64
//l_max = 12
for (i=0; i<etaLatLen; i++) {
for (j=0; j < etaLonLen; j++) {
loadingEta = 0.0;
for (l=0; l<l_max+1; l++) {
for (m=0; m<=l; m++) {
loadingEta += etaLegendreArray[i][l][m] * (SH_C[l][m]*etaCosMLon[j][m] + SH_S[l][m]*etaSinMLon[j][m]);
}
}
etaNewArray[i][j] = loadingEta;
}
}
return 1;
}
我一直试图改变循环次序以加快速度,但无济于事。任何帮助将非常感激。谢谢!
编辑1:
所有五个数组都在我的类的构造函数中分配,如下所示:
etaLegendreArray = new double**[etaLatLen];
for (int i=0; i<etaLatLen; i++) {
etaLegendreArray[i] = new double*[l_max+1];
for (int l=0; l<l_max+1; l++) {
etaLegendreArray[i][l] = new double[l_max+1];
}
}
SH_C = new double*[l_max+1];
SH_S = new double*[l_max+1];
for (int i=0; i<l_max+1; i++) {
SH_C[i] = new double[l_max+1];
SH_S[i] = new double[l_max+1];
}
etaCosMLon = new double*[etaLonLen];
etaSinMLon = new double*[etaLonLen];
for (int j=0; j<etaLonLen; j++) {
etaCosMLon[j] = new double[l_max+1];
etaSinMLon[j] = new double[l_max+1];
}
如果这些是一维数组而不是多维数,也许会更好?
答案 0 :(得分:1)
在这里跳进X-Y地区。让我们尝试加速数据访问,而不是加快算法速度。
etaLegendreArray = new double**[etaLatLen];
for (int i=0; i<etaLatLen; i++) {
etaLegendreArray[i] = new double*[l_max+1];
for (int l=0; l<l_max+1; l++) {
etaLegendreArray[i][l] = new double[l_max+1];
}
}
不创建double
的3D数组。它创建了指向double
数组指针数组的指针数组。每个阵列都有自己的内存块,谁知道它将存储在哪里。这导致数据结构具有所谓的“poor spacial locality”。结构的所有部分可以遍布整个地方。在3D阵列中,您可以跳到三个不同的位置,只是为了找出您的价值所在。
因为模拟3D阵列所需的许多存储块可能彼此无法靠近,所以CPU可能无法提前有效地加载缓存(高速存储器)并且必须停止它的有用工作。做和等待访问较慢的存储,可能更频繁地RAM。这是一个很好的高级article on how much this can hurt表演。
另一方面,如果整个数组在一个内存块中,是“连续的”,CPU可以读取更大的内存块,也许所有内存,它需要一次性进入缓存。此外,如果编译器知道程序将使用的内存全部在一个大块中,它可以执行各种常规优化,使您的程序更快。
那么我们如何获得一个所有内存块的3D数组呢?如果尺寸是静态的,这很容易
double etaLegendreArray[SIZE1][SIZE2][SIZE3];
这看起来不是你的情况,所以你要做的是分配1D数组,因为它将是一个连续的内存块。
double * etaLegendreArray= new double [SIZE1*SIZE2*SIZE3];
手动进行数组索引数学
etaLegendreArray[(x * SIZE2 + y) * SIZE3 + z] = data;
看起来所有额外的数学应该慢一点,对吧?事实证明,每次使用[]
时,编译器都会隐藏看起来很像你的数学。你几乎没有失去任何东西,当然也没有失去一个不必要的cache miss。
但是在整个地方重复这个数学是疯了,迟早你会搞砸尽管可读性的消耗不会让你先死亡,所以你真的想把1D数据包装成一个class to helper为你处理数学。一旦你这样做,你也可以让那个类处理分配和释放,这样你就可以利用all that RAII goodness。遍布for
和new
的{{1}}圈不再有delete
个圈。它全部被包裹起来并用弓打结。
Here is an example of a 2D Matrix class easily extendable to 3D.将以可预测且缓存友好的方式处理您可能需要的基本功能。
答案 1 :(得分:0)
如果CPU支持并且编译器足够优化,你可能会从the C99 fma
(融合乘法 - 加法)函数中获得一些小的收益,来转换你的两步操作(乘法,然后加)一步到位的操作。它还可以提高准确性,因为对融合操作只进行一次浮点舍入,而不是一次乘法和一次加法。
假设我正确读取它,你可以改变你最内层循环的表达式:
loadingEta += etaLegendreArray[i][l][m] * (SH_C[l][m]*etaCosMLon[j][m] + SH_S[l][m]*etaSinMLon[j][m]);
to(注意暂不使用+=
,它已合并到fma
)中:
loadingEta = fma(etaLegendreArray[i][l][m], fma(SH_C[l][m], etaCosMLon[j][m], SH_S[l][m]*etaSinMLon[j][m]), loadingEta);
我不希望任何神奇的性能,但它可能会有所帮助(再次,只有优化足够让编译器内联硬件指令才能完成工作;如果它调用库函数,那么你'将失去对函数调用开销的任何改进)。同样,它应该通过避免你发生的两个舍入步骤来提高准确性。
请注意some compilers with appropriate compilation flags, they'll convert your original code to hardware FMA instructions for you;如果这是一个选项,我会选择它,因为(正如你所看到的)fma
函数往往会降低代码的可读性。
您的编译器也可能提供浮点指令的矢量化版本,这可能会有效地提高性能(请参阅自动转换为FMA的上一个链接)。
大多数其他改进都需要有关目标的更多信息,正在使用的输入数组的性质等。简单的线程可能会获得一些东西,OpenMP pragma可能是一种简化并行化循环的方法(s) )。