【问题标题】:Matrix computations in CC中的矩阵计算
【发布时间】:2018-04-12 15:17:54
【问题描述】:

我最近注意到,在 C 中访问矩阵的方式看似很小的变化可能会对性能产生很大影响。 例如,假设我们有这两个 C 代码片段。这个:

for(i = 0; i < 2048; i++)
{
    for(j = 0; j < 2048; j++) {
            Matrix[i][j] = 9999;    
    }
}

还有这个:

for(j = 0; j < 2048; j++)
{
    for(i = 0; i < 2048; i++) {
            Matrix[i][j] = 9999;    
    }
}

第二个版本比第一个版本慢 2 倍。为什么?我认为这与内存管理有关:在每个循环中,第一个版本访问内存中彼此相邻的位置,而第二个版本必须“跳转”到每个循环中的不同区域。 这种直觉对吗? 此外,如果我使矩阵变小(例如 64x64),那么性能没有差异。为什么? 如果有人能提供一个直观而严谨的解释,我将不胜感激。 顺便说一句,我使用的是 Ubuntu 14.04 LTS。

【问题讨论】:

  • 你的直觉可能是对的,第一个版本可能对现金更友好。查看生成的汇编代码。
  • @MichaelWalz s/cash/cache
  • @Code-Apprentice s/cash/cache/g

标签: c matrix memory


【解决方案1】:
        for(i=0;i<2048;i++)
        {
                for(j=0;j<2048;j++) {
                        Matrix[i][j]=9999;    
                }
        }

这种形式使用 L1、L2 和 L3 缓存对齐。当您在Matrix[i][j] 上循环使用j 时,元素Matrix[i][0]Matrix[i][1]...a.s.o。在连续地址处对齐(实际上是在与sizeof(Matrix[i][0])) 不同的地址处,因此Matrix[i][0] 的访问会将下一个变量Matrix[i][1] 带入高速缓存。

另一方面,

        for(j=0;j<2048;j++)
        {
                for(i=0;i<2048;i++) {
                        Matrix[i][j]=9999;    
                }
        }

内部循环按Matrix[0][j]Matrix[1][j]...a.s.o 的顺序访问。 Matrix[1][j]的地址是Matrix[0][j]+2048*sizeof(Matrix[0][0])——假设你为数组Matrix[0]分配了2048个条目。

所以Matrix[0][j] 位于Matrix[1][j] 之外的另一个缓存块中,因此需要获取在 RAM 中而不是在缓存中进行访问。

在第二种情况下,每次迭代都会访问 RAM。

【讨论】:

    【解决方案2】:

    这是缓存!这是缓存!

    为了形象化,将内存想象成一个线性数组...

    通过定义一个二维数组:

    uint8_t Matrix[4][4]
    

    你只是在说:

    allocate 16 bytes, and access them as a 2D array, 4x4
    

    为了简单起见,这个例子假设一个 4 字节的缓存:

    如果 CPU 的缓存只能容纳 4 个字节,那么接近 [0][0][1][0][2][0]、... 形式的数组将导致 每个 上的缓存未命中> 访问 - 要求我们访问 RAM(这很昂贵)16 次!

    [0][0][0][1][0][2], ... 形式接近数组将允许完全访问 2D 数组,只有 4 次缓存未命中。


    这个例子非常简单——现代系统几乎肯定会有一个 L1 和 L2 缓存,而且许多现在也实现了 L3 缓存。

    随着您离处理器内核越来越远,内存变得越来越大并且越来越慢。例如:

    1. L1 缓存(小,非常非常快)
    2. 二级缓存
    3. 三级缓存(?)
    4. 内存
    5. 持久存储(例如:HDD - 巨大,但相对、非常、非常慢)。

    【讨论】:

      【解决方案3】:

      这都是关于缓存的。在后一种中,您基本上是按顺序读取内存。在第一种情况下,您在每次阅读之间进行跳远。

      计算机中有电路在读取时存储附近的数据,因为附近的数据很可能很快就会被读取。您无法控制这些电路的工作方式。你所能做的就是根据他们的行为调整你的代码。

      【讨论】:

        【解决方案4】:

        它与locality of referenceCPU cache 有关。所以它主要是特定于处理器的(而不是特定于操作系统的)。

        缓存未命中的代价可能非常高(typically,访问 DRAM 模块上的数据需要数百纳秒 - 足以从 L1 I-cache 执行一百个机器指令),但访问 L1 缓存只需要一个或一个几纳秒)。

        另请阅读thisthat。有时(但并非总是)使用__builtin_prefetch可能会提高性能(但通常GCC 编译器可以通过适当地发出PREFETCH machine instructions 来优化比你做的更好)。但是使用 __builtin_prefetch 不当或过于频繁会损害性能。

        不要忘记在编译器中启用优化,因此至少在基准测试之前使用gcc -Wall -O2 -march=native 进行编译(或者甚至使用-O3 而不是-O2 ...)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-05-17
          • 2011-09-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多