Why is my program slow when looping over exactly 8192 elements?
以下是有关程序的摘录。矩阵
然后,你做一个矩阵
1 2 3 4 5 6 7 8 | for(i=1;i<SIZE-1;i++) for(j=1;j<SIZE-1;j++) { res[j][i]=0; for(k=-1;k<2;k++) for(l=-1;l<2;l++) res[j][i] += img[j+l][i+k]; res[j][i] /= 9; } |
这就是节目的全部内容。为了完整起见,下面是前面的内容。后面没有代码。如您所见,这只是初始化。
1 2 3 4 5 6 7 | #define SIZE 8192 float img[SIZE][SIZE]; // input image float res[SIZE][SIZE]; //result of mean filter int i,j,k,l; for(i=0;i<SIZE;i++) for(j=0;j<SIZE;j++) img[j][i] = (2*j+i)%8196; |
基本上,当大小是2048的倍数时,此程序速度较慢,例如执行时间:
1 2 3 | SIZE = 8191: 3.44 secs SIZE = 8192: 7.20 secs SIZE = 8193: 3.18 secs |
编译器是gcc。据我所知,这是因为记忆管理,但我对那个主题不太了解,这就是我在这里问的原因。
另外,如何解决这个问题也很好,但是如果有人能解释这些执行时间,我已经很高兴了。
我已经知道malloc/free,但问题不在于使用的内存量,而在于执行时间,所以我不知道这有什么帮助。
这一差异是由以下相关问题的相同超级对齐问题造成的:
- 为什么512x512的矩阵转置要比513x513的矩阵转置慢得多?
- 矩阵乘法:矩阵大小差小,时间差大
但这只是因为代码还有一个问题。
从原始循环开始:
1 2 3 4 5 6 7 8 | for(i=1;i<SIZE-1;i++) for(j=1;j<SIZE-1;j++) { res[j][i]=0; for(k=-1;k<2;k++) for(l=-1;l<2;l++) res[j][i] += img[j+l][i+k]; res[j][i] /= 9; } |
首先要注意的是,这两个内部循环是微不足道的。它们可以展开如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | for(i=1;i<SIZE-1;i++) { for(j=1;j<SIZE-1;j++) { res[j][i]=0; res[j][i] += img[j-1][i-1]; res[j][i] += img[j ][i-1]; res[j][i] += img[j+1][i-1]; res[j][i] += img[j-1][i ]; res[j][i] += img[j ][i ]; res[j][i] += img[j+1][i ]; res[j][i] += img[j-1][i+1]; res[j][i] += img[j ][i+1]; res[j][i] += img[j+1][i+1]; res[j][i] /= 9; } } |
所以剩下两个我们感兴趣的外环。
现在我们可以看到问题在这个问题中是相同的:为什么循环的顺序会影响在二维数组上迭代时的性能?
您正在按列而不是按行迭代矩阵。
为了解决这个问题,您应该交换这两个循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | for(j=1;j<SIZE-1;j++) { for(i=1;i<SIZE-1;i++) { res[j][i]=0; res[j][i] += img[j-1][i-1]; res[j][i] += img[j ][i-1]; res[j][i] += img[j+1][i-1]; res[j][i] += img[j-1][i ]; res[j][i] += img[j ][i ]; res[j][i] += img[j+1][i ]; res[j][i] += img[j-1][i+1]; res[j][i] += img[j ][i+1]; res[j][i] += img[j+1][i+1]; res[j][i] /= 9; } } |
这完全消除了所有的非顺序访问,因此您不再随机减慢2的大功率。
核心I7 [email protected] GHz
原代码:
1 2 3 | 8191: 1.499 seconds 8192: 2.122 seconds 8193: 1.582 seconds |
互换的外部回路:
1 2 3 | 8191: 0.376 seconds 8192: 0.357 seconds 8193: 0.351 seconds |
下面的测试是用Visual C++编译器完成的,因为默认的QT创建者安装使用它(我猜没有优化标志)。当使用gcc时,Mystical的版本和我的"优化"代码之间没有很大的区别。因此,得出的结论是编译器优化比人类(最终是我)更好地处理微观优化。我把剩下的答案留作参考。
用这种方法处理图像是不有效的。最好使用一维数组。处理所有像素是在一个循环中完成的。可以使用以下方法随机访问点:
1 | pointer + (x + y*width)*(sizeOfOnePixel) |
在这种特殊情况下,最好水平计算和缓存三个像素组的总和,因为每个像素组使用三次。
我做了一些测试,我认为值得分享。每个结果平均为五次测试。
用户原始代码1615209:
1 2 | 8193: 4392 ms 8192: 9570 ms |
神秘版:
1 2 | 8193: 2393 ms 8192: 2190 ms |
使用一维数组进行两次传递:第一次传递用于水平和,第二次传递用于垂直和平均。带三个指针的二通寻址,仅按如下方式递增:
1 2 3 4 5 6 7 8 9 10 | imgPointer1 = &avg1[0][0]; imgPointer2 = &avg1[0][SIZE]; imgPointer3 = &avg1[0][SIZE+SIZE]; for(i=SIZE;i<totalSize-SIZE;i++){ resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9; } 8193: 938 ms 8192: 974 ms |
使用一个一维数组并按如下方式寻址的两次传递:
1 2 3 4 5 6 | for(i=SIZE;i<totalSize-SIZE;i++){ resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9; } 8193: 932 ms 8192: 925 ms |
一次通过缓存水平和仅向前一行,因此它们保持在缓存中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Horizontal sums for the first two lines for(i=1;i<SIZE*2;i++){ hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1]; } // Rest of the computation for(;i<totalSize;i++){ // Compute horizontal sum for next line hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1]; // Final result resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9; } 8193: 599 ms 8192: 652 ms |
结论:
- 使用多个指针和增量没有任何好处(我以为会更快)
- 缓存水平和比多次计算要好。
- 两次传球不比三次快,只比两次快。
- 单次通过和缓存中间结果都可以实现3.6倍的速度
我相信可以做得更好。
注释请注意,我写这个答案是为了解决一般的性能问题,而不是Mystical优秀答案中解释的缓存问题。一开始只是伪代码。我被要求在评论中做测试…这里是一个完全重构的测试版本。