我同意转置以获得更好的缓存(但最后请参阅我的 cmets),还有更多工作要做,所以让我们看看我们可以用完整的功能做什么......
原始函数,供参考(为了我的理智而进行了一些整理):
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 来代替它。也许图书馆已经有一个)。
同样,虽然我猜你不能改变签名,但你可以让函数只接受一个参数并修改它。当 x 或 y 已设置时,永远不会使用 b。这也意味着您可以摆脱运行 ~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 成为指针数组,否则它似乎可以与重构)。