这是有关程序的摘录。矩阵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,但问题不在于使用的内存量,而在于执行时间,所以我不知道这会有什么帮助。


当前回答

以下测试是用Visual C++编译器完成的,因为默认的Qt Creator安装使用了该编译器(我猜没有优化标志)。当使用GCC时,Mystic的版本和我的“优化”代码之间没有太大的区别。因此,结论是编译器优化比人类(最后是我)更好地照顾微观优化。我将剩下的答案留作参考。


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

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

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

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

用户1615209的原始代码:

8193: 4392 ms
8192: 9570 ms

神秘的版本:

8193: 2393 ms
8192: 2190 ms

使用1D阵列的两遍:第一遍用于水平求和,第二遍用于垂直求和和平均。具有三个指针的两遍寻址,仅递增如下:

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

使用1D阵列和如下寻址的两遍:

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倍的速度

我相信有可能做得更好。

注释请注意,我写这个答案是为了解决一般性能问题,而不是Mystic的优秀答案中解释的缓存问题。一开始只是伪代码。我被要求在评论中做测试。。。这是一个完全重构的测试版本。

其他回答

以下测试是用Visual C++编译器完成的,因为默认的Qt Creator安装使用了该编译器(我猜没有优化标志)。当使用GCC时,Mystic的版本和我的“优化”代码之间没有太大的区别。因此,结论是编译器优化比人类(最后是我)更好地照顾微观优化。我将剩下的答案留作参考。


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

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

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

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

用户1615209的原始代码:

8193: 4392 ms
8192: 9570 ms

神秘的版本:

8193: 2393 ms
8192: 2190 ms

使用1D阵列的两遍:第一遍用于水平求和,第二遍用于垂直求和和平均。具有三个指针的两遍寻址,仅递增如下:

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

使用1D阵列和如下寻址的两遍:

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倍的速度

我相信有可能做得更好。

注释请注意,我写这个答案是为了解决一般性能问题,而不是Mystic的优秀答案中解释的缓存问题。一开始只是伪代码。我被要求在评论中做测试。。。这是一个完全重构的测试版本。

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

为什么转换512x512的矩阵比转换513x513的矩阵慢得多?矩阵乘法:矩阵大小差异小,定时差异大

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

从原始循环开始:

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

这完全消除了所有非顺序访问,因此您不再在2的大幂幂上获得随机减速。


核心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