我想优化这个简短的循环

12

我希望优化这个简单的循环:

unsigned int i;
while(j-- != 0){ //j is an unsigned int with a start value of about N = 36.000.000
   float sub = 0;
   i=1;
   unsigned int c = j+s[1];
   while(c < N) {
       sub += d[i][j]*x[c];//d[][] and x[] are arrays of float
       i++;
       c = j+s[i];// s[] is an array of unsigned int with 6 entries.
   }
   x[j] -= sub;                        // only one memory-write per j
}

使用4000 MHz AMD Bulldozer,循环的执行时间约为一秒钟。我考虑过SIMD和OpenMP(通常用于提高速度),但这个循环是递归的。

有什么建议吗?


2
能否提供更多上下文信息?“d [] []”和“x []”是否声明为__restrict__ij实际上有多大?“这个循环是递归的……”--请提供更多上下文信息。 - Brian Cain
优化后的编译器输出是什么样子? - andy256
8
此外,从一个分析器开始,以确定瓶颈出现的位置。 - Brian Cain
1
这不是一个递归循环(没有这种东西)。它是一个嵌套在另一个循环中的循环。 - Nikos C.
@1,Brian:我之前写过,j的值大约为36,000,000。因为s[7] = N+1,所以i的最大值为7。该循环是递归的,因为它在下一次迭代中使用存储在x[j]中的值。我曾尝试在Win7上使用gprof,但没有得到合理的结果。我可以给你更多上下文信息,但由于这是我在这里发布的第一篇帖子,我不知道发布源代码的最大限制等等。感谢您的快速回复,Ingo。 - Ingo
3
有一些微调可以做,但要真正提升性能,尝试在更大的层面上优化代码(由于你没有发布足够的周围代码,我无法提供任何建议)。 - Dave
3个回答

10

我认为你可能想要转置矩阵 d -- 这意味着将其存储在可以交换索引的方式中 -- 将 i 作为外部索引:

    sub += d[j][i]*x[c];

替代

    sub += d[i][j]*x[c];

这应该会导致更好的缓存性能。


很好的发现,我认为这实际上是导致每个循环花费极长时间的原因。 - smac89
我尝试交换循环,但是这样会有更多的内存写入,导致速度变慢。我还尝试了使用OpenMP来交换循环,但由于缓存冲突,速度变得更慢了。矩阵转置可能是可行的,但不适用于此算法的其他部分。 - Ingo
好的!也许我应该给你更多信息。这个循环是CholeskyBackSolve的一部分,位于此文件中:http://code.google.com/p/rawtherapee/source/browse/rtengine/EdgePreservingDecomposition.cc。我上面发布的版本已经进行了一些优化,并且替换了从第335行开始的while循环。Ingo - Ingo
转置、利用良好的缓存局部性进行计算,然后再次转置可能仍然更快。 - paddy
我会尝试转置和反转置。 - Ingo
1
这是虚拟内存计算系统中最古老的问题。始终要问自己数组是按行主序还是列主序存储,并处理它们,以便访问相邻元素而不是相距很远的元素。 - Ross Patterson

6

我同意为了更好的缓存而进行转置 (但请看我的评论),还有更多工作要做,所以让我们看看我们可以用完整函数做些什么...

参考原始函数(已经进行了一些整理):

void MultiDiagonalSymmetricMatrix::CholeskyBackSolve(float *x, float *b){
    //We want to solve L D Lt x = b where D is a diagonal matrix described by Diagonals[0] and L is a unit lower triagular matrix described by the rest of the diagonals.
    //Let D Lt x = y. Then, first solve L y = b.

    float *y = new float[n];
    float **d = IncompleteCholeskyFactorization->Diagonals;
    unsigned int *s = IncompleteCholeskyFactorization->StartRows;
    unsigned int M = IncompleteCholeskyFactorization->m;
    unsigned int N = IncompleteCholeskyFactorization->n;
    unsigned int i, j;
    for(j = 0; j != N; j++){
        float sub = 0;
        for(i = 1; i != M; i++){
            int c = (int)j - (int)s[i];
            if(c < 0) break;
            if(c==j) {
                sub += d[i][c]*b[c];
            } else {
                sub += d[i][c]*y[c];
            }
        }
        y[j] = b[j] - sub;
    }

    //Now, solve x from D Lt x = y -> Lt x = D^-1 y
    // Took this one out of the while, so it can be parallelized now, which speeds up, because division is expensive
#pragma omp parallel for
    for(j = 0; j < N; j++){
        x[j] = y[j]/d[0][j];
    }

    while(j-- != 0){
        float sub = 0;
        for(i = 1; i != M; i++){
            if(j + s[i] >= N) break;
            sub += d[i][j]*x[j + s[i]];
        }
        x[j] -= sub;
    }
    delete[] y;
}

由于关于并行分割可以提高速度的评论(尽管只有O(N)),我假设函数本身被频繁调用。那么为什么要分配内存呢?只需将x标记为__restrict__,并在所有地方将y更改为x(__restrict__是来自C99的GCC扩展名。您可能希望为其使用define。也许该库已经有了一个)。
同样地,虽然我猜您无法更改签名,但可以使函数仅接受单个参数并进行修改。当设置了x或y时,b从未被使用。这也意味着您可以在第一个循环中摆脱运行约N*M次的分支。如果必须使用两个参数,请在开始时使用memcpy。
d为什么是指针数组?必须吗?这似乎太深入原始代码,所以我不会去动它,但是如果有任何扁平化存储数组的可能性,即使您无法转置它,它也将提高速度(乘法,加法,解除引用比解除引用,加法,解除引用更快)。
因此,新代码:
void MultiDiagonalSymmetricMatrix::CholeskyBackSolve(float *__restrict__ x){
    // comments removed so that suggestions are more visible. Don't remove them in the real code!
    // these definitions got long. Feel free to remove const; it does nothing for the optimiser
    const float *const __restrict__ *const __restrict__ d = IncompleteCholeskyFactorization->Diagonals;
    const unsigned int *const __restrict__ s = IncompleteCholeskyFactorization->StartRows;
    const unsigned int M = IncompleteCholeskyFactorization->m;
    const unsigned int N = IncompleteCholeskyFactorization->n;
    unsigned int i;
    unsigned int j;
    for(j = 0; j < N; j++){ // don't use != as an optimisation; compilers can do more with <
        float sub = 0;
        for(i = 1; i < M && j >= s[i]; i++){
            const unsigned int c = j - s[i];
            sub += d[i][c]*x[c];
        }
        x[j] -= sub;
    }

    // Consider using processor-specific optimisations for this
#pragma omp parallel for
    for(j = 0; j < N; j++){
        x[j] /= d[0][j];
    }

    for( j = N; (j --) > 0; ){ // changed for clarity
        float sub = 0;
        for(i = 1; i < M && j + s[i] < N; i++){
            sub += d[i][j]*x[j + s[i]];
        }
        x[j] -= sub;
    }
}

现在看起来更整洁了,如果没有其他东西,内存分配的缺乏和减少的分支是一种提升。如果您可以将s更改为在末尾包括额外的UINT_MAX值,则可以删除更多的分支(包括i<M检查,这些检查再次运行~ N * M次)。

现在我们不能再并行进行任何循环,也不能组合循环。如其他答案所建议的那样,重新排列d将会有所提高。但是……重新排列d所需的工作与执行循环的工作具有完全相同的缓存问题。而且还需要分配内存。不好。进一步优化的唯一选择是:更改IncompleteCholeskyFactorization->Diagonals本身的结构,这可能需要进行大量更改,或者找到一个使用此顺序数据效果更好的不同算法。

如果您想进一步进行优化,则需要影响代码的相当大部分(除非Diagonals作为指针数组存在充分理由,否则看起来需要进行重构)。


1
如果你使用 UINT_MAX,请记得重新排列第二个方程以避免溢出:N - j > s[i] - Dave
非常感谢您详细的答复!我按照您的建议消除了y并限制了x。这使速度提高了一点。最大的加速是通过完全不同的方法实现的。我改变了对角线内存分配的方式,以便每个对角线都从不同的64字节边界开始。这使速度提高了2倍:-) 只有Bulldozer的4路关联L1缓存真的很混乱... 现在我将进一步进行可能会对该代码进行完全重新设计(这不是我写的,我只是试图进行优化)。 - Ingo

2
我希望回答自己的问题:糟糕的性能是由于缓存冲突导致的,因为(至少)Win7将大内存块对齐到相同的边界。在我的情况下,所有缓冲区的地址都具有相同的对齐方式(所有缓冲区的bufferadress%4096相同),因此它们落入L1缓存的相同cacheset中。我更改了内存分配以使缓冲区对齐到不同的边界以避免缓存冲突,并获得了2倍的加速。感谢所有答案,特别是来自Dave的答案!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接