我想优化这个短循环

时间:2013-08-15 21:55:11

标签: c++ performance algorithm

我想优化这个简单的循环:

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,循环执行时间约为1秒。我想过SIMD和OpenMP(我通常用来提高速度),但这个循环是递归的。

有什么建议吗?

3 个答案:

答案 0 :(得分:10)

认为您可能想要转置矩阵d - 意味着以这样的方式存储它以便您可以交换索引 - 使我成为外部索引:

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

而不是

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

这应该会带来更好的缓存性能。

答案 1 :(得分: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__是GCC扩展名,取自C99。您可能希望使用{define 1}}对于它。也许图书馆已经有了一个)。

同样,虽然我猜您无法更改签名,但您可以使该函数只接受一个参数并对其进行修改。在bx设置后,永远不会使用y。这也意味着你可以摆脱第一个循环中的分支,它运行~N * M次。如果您必须有2个参数,请在开始时使用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作为指针数组的充分理由,它似乎可以做一个重构)。

答案 2 :(得分:2)

我想回答一下我自己的问题:糟糕的性能是由于(至少)Win7将大内存块对齐到同一边界这一事实引起的缓存冲突错误。在我的情况下,对于所有缓冲区,地址具有相同的对齐方式(bufferadress%4096对于所有缓冲区都相同),因此它们属于L1缓存的相同缓存集。我更改了内存分配以将缓冲区对齐到不同的边界,以避免缓存冲突未命中并获得因子2的加速。感谢所有答案,尤其是Dave的答案!