当正好遍历8192个元素时,为什么我的程序运行缓慢?


755

这是有问题的程序的摘录。矩阵img[][]的大小为SIZE×SIZE,并初始化为:

img[j][i] = 2 * j + i

然后,创建一个矩阵res[][],并将此处的每个字段作为img矩阵中周围9个字段的平均值。为简单起见,边框保留为0。

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;
}

这就是程序的全部内容。为了完整起见,这是之前的内容。之后没有代码。如您所见,这只是初始化。

#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;

基本上,当SIZE为2048的倍数时,该程序运行缓慢,例如执行时间:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

编译器是GCC。据我所知,这是由于内存管理引起的,但是我对这个主题并不了解太多,这就是为什么我在这里问。

同样如何解决这个问题也很好,但是如果有人可以解释这些执行时间,我已经很高兴了。

我已经知道malloc / free了,但是问题不在于所使用的内存量,而仅仅是执行时间,所以我不知道这会如何帮助。


67
@bokan当大小是高速缓存的临界跨度的倍数时发生。
Luchian Grigore'9

5
@Mysticial,没关系,它暴露了同样的问题;代码可能会有所不同,但基本上两个问题都在大约相同的时间问到(它们的标题肯定相似)。
Griwes 2012年

33
如果要获得高性能,则不应使用二维数组处理图像。考虑所有像素都是原始像素,并像一维数组一样处理它们。分两次通过此模糊处理。首先使用3个像素的滑动总和添加周围像素的值:slideSum + = src [i + 1] -src [i-1]; dest [i] = slideSum;。然后垂直进行相同操作并同时分割:dest [i] =(src [i-width] + src [i] + src [i + width])/ 9。www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan 2012年

8
实际上,这里有两件事。不只是超级对齐。
Mysticial 2012年

7
(对于您的回答,只不过是个小问题。对于第一个代码段,如果您所有的for循环都带有花括号,那就太好了。)
Trevor Boyd Smith

Answers:


954

差异是由以下相关问题中的同一超级对齐问题引起的:

但这仅仅是因为代码还有另一个问题。

从原始循环开始:

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;
}

首先请注意,两个内部循环是微不足道的。它们可以按以下方式展开:

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;
    }
}

这样就剩下了我们感兴趣的两个外部循环。

现在我们可以看到这个问题的问题是相同的:为什么在2D数组上进行迭代时循环的顺序会影响性能?

您正在逐列而不是逐行迭代矩阵。


要解决此问题,您应该互换两个循环。

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;
    }
}

这样就完全消除了所有非顺序访问,因此您不再需要大二乘方来随机降低速度。


酷睿i7 920 @ 3.5 GHz

原始代码:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

互换的外部环路:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
我还将注意到展开内部循环对性能没有影响。编译器可能会自动执行。我展开它们的唯一目的是摆脱它们,以便更轻松地发现外部循环的问题。
Mysticial 2012年

29
您可以通过沿每一行缓存总和来将代码加快三倍。但这和其他优化不在原始问题的范围内。
埃里克·波斯特皮希尔

33
@ClickUpvote这实际上是硬件(缓存)问题。它与语言无关。如果您以其他任何将本机代码编译或JIT编译为本机代码的语言进行了尝试,则可能会看到相同的效果。
Mysticial 2012年

19
@ClickUpvote:您似乎被误导了。那个“第二个循环”只是神秘主义者用手展开内部循环。这几乎是编译器肯定会做的事情,而Mystical只是这样做是为了使外部循环的问题更加明显。这绝不是您应该自己做的事情。
莉莉·巴拉德

154
这是一个很好的SO完美答案的例子:引用相似的问题,分步说明如何使用它,说明问题,说明如何解决问题,具有良好的格式,甚至是运行代码的示例在您的机器上。感谢您的贡献。
MattSayar 2012年

57

默认的Qt Creator安装使用Visual C ++编译器进行了以下测试(我猜没有优化标志)。使用GCC时,Mystical的版本与我的“优化”代码之间没有太大区别。因此得出的结论是,编译器优化比人工优化更​​能解决微优化问题(最后是我)。我将其余答案留作参考。


这样处理图像效率不高。最好使用一维数组。一个循环即可完成所有像素的处理。可以使用以下方法来随机访问点:

pointer + (x + y*width)*(sizeOfOnePixel)

在这种情况下,最好水平计算和缓存三个像素组的总和,因为每个像素组使用了三次。

我做了一些测试,我认为值得分享。每个结果是五个测试的平均值。

用户1615209的原始代码:

8193: 4392 ms
8192: 9570 ms

神秘版本:

8193: 2393 ms
8192: 2190 ms

使用一维数组进行两次遍历:第一次遍历用于水平求和,第二遍遍用于垂直求和和平均值。具有三个指针的两次通过寻址,并且仅递增如下:

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

使用一维数组两次寻址,如下所示:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

一遍缓存水平仅相加前一行,因此它们保留在缓存中:

// 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出色答案中解释的缓存问题。一开始它只是伪代码。要求我在注释中进行测试...这是带有测试的完全重构版本。


9
“我认为速度至少要快3倍”-是否希望通过一些指标或引用来支持该主张?
亚当·罗森菲尔德

8
@AdamRosenfield“我认为” =假设!=“是” =声明。我对此没有度量标准,我想看一下测试。但是我的要求每个像素有7个增量,2个子级,2个相加和一个div。每个循环使用的本地变量少于CPU中的寄存器。另一个需要7增量,6减量,1 div和10至20 mul之间的地址,具体取决于编译器的优化。同样,循环中的每条指令都需要上一条指令的结果,这放弃了奔腾超标量体系结构的好处。因此它必须更快。
bokan 2012年

3
原始问题的答案全部与内存和缓存效果有关。OP的代码之所以如此慢,是因为它的内存访问模式是按列而不是按行进行的,而这种方式的引用缓存位置非常差。这在8192时特别糟糕,因为随后连续的行最终会在直接映射的高速缓存或具有低关联性的高速缓存中使用相同的高速缓存行,因此高速缓存未命中率甚至更高。通过大大增加缓存的局部性,交换循环可以极大地提高性能。
亚当·罗森菲尔德

1
做得好,这些数字令人印象深刻。如您所见,这完全与内存性能有关-使用多个具有增量的指针并没有带来任何好处。
亚当·罗森菲尔德

2
@AdamRosenfield我今天早上很担心,因为我无法复制测试。似乎仅使用Visual C ++编译器才能提高性能。使用gcc,只有很小的差异。
博坎2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.