【问题标题】:A Cache Efficient Matrix Transpose Program?缓存高效的矩阵转置程序?
【发布时间】:2011-07-09 04:35:32
【问题描述】:

所以转置矩阵的明显方法是使用:

  for( int i = 0; i < n; i++ )

    for( int j = 0; j < n; j++ )

      destination[j+i*n] = source[i+j*n];

但我想要一些可以利用局部性和缓存阻塞的东西。我正在查找它,但找不到可以执行此操作的代码,但有人告诉我它应该是对原始代码的非常简单的修改。有任何想法吗?

编辑:我有一个 2000x2000 矩阵,我想知道如何使用两个 for 循环更改代码,基本上将矩阵拆分为我单独转置的块,例如 2x2 块或 40x40 块,然后查看哪种块大小最有效。

Edit2:矩阵按列主要顺序存储,即矩阵

a1 a2    
a3 a4

存储为a1 a3 a2 a4

【问题讨论】:

  • 当心,因为一个非常热门的现代编译器可能会通过使用完全重新设计代码的强大优化来为您解决这个问题。查看关键字限制。查看所有优化开关,例如 gcc,例如必须从使用 -O3 或 -O2 开始,以最快者为准,并且必须使用开关来支持您的现代 cpu 的完整指令集,例如gcc -march=haswell。不要使用垃圾编译器。 Gcc Intel 和 llvm/clang 都很好。

标签: algorithm caching matrix


【解决方案1】:

我昨天遇到了完全相同的问题。 我最终得到了这个解决方案:

void transpose(double *dst, const double *src, size_t n, size_t p) noexcept {
    THROWS();
    size_t block = 32;
    for (size_t i = 0; i < n; i += block) {
        for(size_t j = 0; j < p; ++j) {
            for(size_t b = 0; b < block && i + b < n; ++b) {
                dst[j*n + i + b] = src[(i + b)*p + j];
            }
        }
    }
}

这比我机器上的明显解决方案快 4 倍。

此解决方案负责处理尺寸不是块大小倍数的矩形矩阵。

如果 dst 和 src 是同一个方阵,则应该真正使用 in place 函数:

void transpose(double*m,size_t n)noexcept{
    size_t block=0,size=8;
    for(block=0;block+size-1<n;block+=size){
        for(size_t i=block;i<block+size;++i){
            for(size_t j=i+1;j<block+size;++j){
                std::swap(m[i*n+j],m[j*n+i]);}}
        for(size_t i=block+size;i<n;++i){
            for(size_t j=block;j<block+size;++j){
                std::swap(m[i*n+j],m[j*n+i]);}}}
    for(size_t i=block;i<n;++i){
        for(size_t j=i+1;j<n;++j){
            std::swap(m[i*n+j],m[j*n+i]);}}}

我使用的是 C++11,但这很容易翻译成其他语言。

【讨论】:

  • 在第一个函数中你在哪里使用“p”?
  • 这行应该是“dst[j*n + i + b] = src[(i + b)*n + j];”保持不变?
  • @Lingviston 你又完全正确了。我修好了这条线。
【解决方案2】:

Steve Jessop 提到了缓存遗忘矩阵转置算法。 作为记录,我想分享一个缓存遗忘矩阵转置的可能实现。

public class Matrix {
    protected double data[];
    protected int rows, columns;

    public Matrix(int rows, int columns) {
        this.rows = rows;
        this.columns = columns;
        this.data = new double[rows * columns];
    }

    public Matrix transpose() {
        Matrix C = new Matrix(columns, rows);
        cachetranspose(0, rows, 0, columns, C);
        return C;
    }

    public void cachetranspose(int rb, int re, int cb, int ce, Matrix T) {
        int r = re - rb, c = ce - cb;
        if (r <= 16 && c <= 16) {
            for (int i = rb; i < re; i++) {
                for (int j = cb; j < ce; j++) {
                    T.data[j * rows + i] = data[i * columns + j];
                }
            }
        } else if (r >= c) {
            cachetranspose(rb, rb + (r / 2), cb, ce, T);
            cachetranspose(rb + (r / 2), re, cb, ce, T);
        } else {
            cachetranspose(rb, re, cb, cb + (c / 2), T);
            cachetranspose(rb, re, cb + (c / 2), ce, T);
        }
    }
}

更多关于缓存遗忘算法的细节可以在here找到。

【讨论】:

  • 附加信息的链接导致 403 Forbidden 错误。
  • 谢谢,我修复了损坏的链接。
  • 链接再次断开。
【解决方案3】:

对于大型矩阵,可能是大型稀疏矩阵,将其分解为更小的缓存友好块(例如,4x4 子矩阵)可能是一个想法。您还可以将子矩阵标记为身份,这将帮助您创建优化的代码路径。

【讨论】:

    【解决方案4】:

    与其在内存中转置矩阵,不如将转置操作折叠到您要对矩阵执行的下一个操作中?

    【讨论】:

    • 绝对值得考虑。一种不错的方法是创建一个对象,该对象在原始矩阵上呈现“视图”,就像数据库中的视图。
    • 不仅仅是值得考虑,@j_random_hacker。短期内的“视图”对象将表示与意图分离,这通常是一个很好的设计策略。它还允许您以类似于数据库查询优化器所做的方式让计算机为事物找出好的算法。
    • 这是个好建议。一个非常小的性能警告:实现视图很可能最终会通过函数指针引入调用,而不是调用已知的固定函数地址。在某些现代机器上,通过未知变量指针调用可能会非常慢。
    【解决方案5】:

    Matrix multiplication 浮现在脑海中,但缓存问题更为明显,因为每个元素都被读取 N 次。

    使用矩阵转置,您可以在单个线性通道中读取,并且无法对其进行优化。但是您可以同时处理多行,以便写入多列并填充完整的缓存行。您只需要三个循环。

    或者反过来做,在线性写入的同时按列读取。

    【讨论】:

    • 确实是个好建议。我不确定哪种方式最快,取决于机器架构。
    【解决方案6】:

    您可能需要四个循环 - 两个循环遍历块,然后另外两个循环执行单个块的转置复制。为简单起见,假设一个块大小除以矩阵的大小,我认为是这样的,尽管我想在信封的背面画一些图片来确定:

    for (int i = 0; i < n; i += blocksize) {
        for (int j = 0; j < n; j += blocksize) {
            // transpose the block beginning at [i,j]
            for (int k = i; k < i + blocksize; ++k) {
                for (int l = j; l < j + blocksize; ++l) {
                    dst[k + l*n] = src[l + k*n];
                }
            }
        }
    }
    

    一个重要的进一步见解是,实际上有一个缓存忽略算法(参见http://en.wikipedia.org/wiki/Cache-oblivious_algorithm,它使用这个确切的问题作为示例)。 “cache-oblivious”的非正式定义是您不需要尝试调整任何参数(在本例中为块大小)以达到良好/最佳的缓存性能。这种情况下的解决方案是通过递归地将矩阵一分为二,然后将两半转置到它们在目的地中的正确位置来进行转置。

    无论实际缓存大小是多少,此递归都会利用它。我预计与您的策略相比,会有一些额外的管理开销,即使用性能实验来直接跳转到缓存真正启动的递归点,并且不再继续。另一方面,您的性能实验可能会给您一个适用于您的机器但不适用于您客户的机器的答案。

    【讨论】:

    • 那么,如果我们有一个块大小为 2 的 7 x 7 矩阵,这个代码会在边缘情况下中断吗?
    • @user635832: 是的,它会的,因为kl 的边界是i+2j+2,对于右下边缘周围的块来说,这将超出实际矩阵的边距为 1,因为 ij 将是 6。最简单的解决方法是将 k 绑定到 k &lt; i+blocksize &amp;&amp; k &lt; n,对于 l 也是如此。除非我遗漏了什么或搞砸了——我根本没有测试过这段代码。
    • 很奇怪,我正在测试这段代码,它似乎工作,但实际上运行速度较慢,缓存效率比没有。也许 2000x2000 矩阵对于效率提升来说太小了?
    • 哦,如果这是作为函数实现的,你有没有将指针参数srcdst标记为restrict?如果您至少在原则上如此,编译器可以为自己重新排序副本,而当srcdst 可能重叠时,它无法做到这一点。很有可能,它比你更了解你的硬件。
    • 更正:我认为编译有问题。您提供的解决方案快 4 倍 :)
    猜你喜欢
    • 1970-01-01
    • 2014-06-09
    • 1970-01-01
    • 2013-07-16
    • 1970-01-01
    • 2016-03-03
    • 2012-01-22
    • 1970-01-01
    • 2018-04-07
    相关资源
    最近更新 更多