缓存友好矩阵移位功能

时间:2016-05-03 15:54:06

标签: c caching optimization matrix

我想将2D方阵的第一行移到最后一行。所以如果我有像A这样的矩阵,我想得到B。

visual of process

我可以使用两个简单的for循环来完成此操作。 E.g。

void shift(int M, int N, int A[M][N]){
    int i, j,temp;
    for (i = 1; i < M; i++){
        for (j = 0; j < N; j++){
            temp=A[i][j];
            A[i][j]=A[i-1][j];
            A[i-1][j]=temp;
        }
    }
}

但我希望尽可能减少缓存未命中数。有关如何做到这一点的任何提示?

2 个答案:

答案 0 :(得分:2)

/* M is the number of rows; N is the number of columns. */
void matrix_shift(int M, int N, int A[M][N]) {
    size_t rowbytes = N * sizeof(int);
    int temprow[N];
    memcpy(temprow, A, rowbytes); // store first row
    memmove(A, A + 1, (M-1) * rowbytes); // shift up
    memcpy(A + (M-1), temprow, rowbytes); // replace last row
}

这使它变得简单并且依赖于应该在任何通用平台上高度优化的例程。复制了一个额外的行,但在方阵矩阵的情况下,这是一个次要的低效率。

答案 1 :(得分:1)

我刚看到你对4x4矩阵的评论。一个4x4 int数组适合单个缓存行(在现代x86 CPU上,缓存行为64B)。在这种情况下,您希望编译器生成类似

的内容
## matrix address in [rdi]
movups    xmm0, [rdi]
movups    xmm1, [rdi+16]
movups    xmm2, [rdi+32]
movups    xmm3, [rdi+48]
movups    [rdi],    xmm1     ; doing all the stores after all the loads avoids any possible false dependency
movups    [rdi+16], xmm2
movups    [rdi+32], xmm3
movups    [rdi+48], xmm0

或者可能更少的AVX 256b加载/存储,但未对齐的AVX可能会更糟。如果阵列是64B对齐的,那么所有加载/存储都不会跨越缓存线边界。因此,2 vmovups ymm个加载,一个vmovups ymm商店,一个vmovups xmm商店(到最后)和一个vextractf128商店(到开头)。

如果你很幸运,当函数内联到具有编译时常量值4的调用者时,John的memcpy会优化到类似的东西。

对于小型阵列,问题不在于缓存未命中,而是如何以尽可能少的开销实现整个副本。我在下面提到的关于引入间接级别的想法并不是一个好主意,因为加载所有数据并将其存储回来真的很便宜。

对于大型矩阵:

如果你在矩阵的末尾留出另一行的空间,你可以将第一行复制到这个额外的空间,并将指针传递给第二行。

这使您可以暂时拥有矩阵的不同视图,但这不是一个可重复的过程。

如果你有一个大缓冲区,你可以继续以这种方式旋转矩阵行,直到你到达保留空间的末尾并且必须将数组复制回缓冲区的顶部。这最大限度地减少了复制开销,但确实意味着您正在触摸一些新内存。

如果行复制开销很大,引入间接级别可能是个好主意。根据在对行进行洗牌后使用它的代码的访问模式,这可能会更糟。这可能是指向行指针数组的用例,而不是普通的2D数组。

您可以而且应该使用一个大的分配为矩阵分配存储空间,而不是分别分配每一行。 C ++ std::vector向量并不理想。初始化int *rows[M]只需要循环&A[i][0],因此它只是数学,而不是多次加载或分配。

通过此间接表访问数组会使用指针追逐替换i*N + j数学:加载rows[i],然后使用j对其进行索引。

当你不需要数组的混乱视图时,你可以直接访问它,但是如果你想能够对数组进行永久性的改组,那么它的所有用户总是必须通过间接访问层