【问题标题】:Optimization via loop blocking in C通过 C 中的循环阻塞进行优化
【发布时间】:2018-11-13 06:03:17
【问题描述】:

我目前正在研究 C 优化,并且过去有一个任务是优化一段代码。在其他优化(展开循环和强度降低)中,我根据缓存大小使用了阻塞(按照英特尔关于此事的教程):

https://software.intel.com/en-us/articles/how-to-use-loop-blocking-to-optimize-memory-use-on-32-bit-intel-architecture.

现在我想我明白为什么这种技术在这种情况下有效,当步幅为 1 时,它将块加载到缓存中,并在访问内存中的下一个位置时减少了未命中的数量。但在我的代码中,dst[dim * jj + ii] 似乎到处乱跳,因为它在最里面的循环中乘以 jj。缓存是如何解决这个问题的? dim 乘以 0 然后 1 然后 2 等等,在某些时候它将超过块可以容纳的值,优化将毫无意义。我理解这个正确吗?

然而,在实践中,当我仅对 jj 变量使用阻塞时,我并没有通过在 iijj 上使用阻塞来提高性能。所以我让它更快,但不知道为什么。现在作业已经过去了,但我还是不明白,很沮丧。 预先感谢您接受这个可能非常愚蠢的问题。

   void transpose(int *dst, int *src, int dim)
   {
      int i, j, dimi, jj,ii;
      dimi = 0;
      for(i=0; i < dim; i+=block_size)
      {
        for(j=0; j<dim; j+=block_size)
        {
          for(ii = i; ii < i+block_size; ii++)
          {
            dimi = dim * ii;
            for(jj = j; jj < j+block_size; jj++)
            {
              dst[dim*jj + ii] =  src[dimi + jj];
            }
          }
        }
      }
    }

【问题讨论】:

  • 什么编译器具有什么优化级别,在什么硬件上? gcc -O3 将自动矢量化,但 IDK 如果它会在这里做得很好。连续存储可能比连续加载更好;然后您可以使用矢量加载+随机播放和矢量存储进行矢量化。但是,编译器可以在 ii 循环(从内部的第二个)而不是内部对循环进行矢量化,并并行生成 4 个 dst[] 结果。 (除了 x86 的 gcc 会进行向量加载并将其解压缩到 4 个存储中。它可以检查 src 和 dst 之间的重叠,但不会。godbolt.org/g/g24ehr IDK clang 对 256 字节的副本做了什么)

标签: c performance cpu-cache micro-optimization


【解决方案1】:

您在 dst 中的空间局部性很差,但是在两个维度的阻塞下,时间和空间上的局部性仍然足够,当您存储下一个 int 时,缓存行通常在 L1d 缓存中仍然很热。

假设dst[dim*jj + ii] 是缓存行中的第一个intdst[dim*jj + ii + 1] 的存储将在同一缓存行中。如果该行在 L1d 缓存中仍然很热,则 CPU 没有花费任何带宽来驱逐脏行 do L2,然后将其带回 L1d 以进行下一次存储。

通过两个维度的阻塞,下一个商店将在block_size 更多商店到dst[ dim*(jj+1..block_size-1) + ii ] 之后发生。 (ii 循环的下一次迭代。)

如果 dimblock_size 都是 2 的幂,则线路可能会因为冲突而被驱逐。相隔 4kiB 的地址在 L1d 中进入相同的集合,尽管 L2 的问题步幅更大。 (英特尔的 L1d 缓存是 32kiB 和 8 路集合关联,因此对于同一个集合只要再多 8 个存储就可能会驱逐一行。但是 L3 缓存使用哈希函数进行集合索引,而不是使用范围的简单模数直接计算地址位。IDK 你的缓冲区有多少位,或者你的整个矩阵可以在你的 L3 缓存中保持热。)

但是,如果 dimblock_size 不是 2 的幂,那么所有 64 组 8 行 64 字节 (L1d) 都会起作用。因此,L1d 缓存中最多可以有 64*8 = 512 条脏线。但请记住,数据仍然是按顺序加载的,这会占用一些空间。 (不多,因为您从每行加载的数据中连续读取 16 个整数,并使用它来脏 16 行不同的目标数据。)

仅在 1 维中阻塞,您在返回目的地线之前会做更多的商店,因此到那时它可能已被驱逐到 L2 或 L3。 p>

顺便说一句,我将您的代码放在 Godbolt 编译器资源管理器 (https://godbolt.org/g/g24ehr) 上,而 x86 的 gcc -O3 不会尝试做任何特别的事情。它使用向量加载到 XMM 寄存器中,并使用 shuffle 解包并执行 4 个单独的 int 存储。

clang6.0 做了一些有趣的事情,包括复制一个 256 字节的块。 IDK 如果它这样做是为了解决别名问题(因为没有int *restrict dst 它不知道 src 和 dst 不会重叠)。


顺便说一句,连续写入和分散读取可能会更好。 (即反转你的内部两个循环,所以ii 在最里面的循环中更改而不是jj)。驱逐脏缓存行比驱逐干净行并稍后重新读取它更昂贵。

【讨论】:

    猜你喜欢
    • 2017-12-26
    • 2013-01-20
    • 1970-01-01
    • 2020-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-30
    • 2017-02-28
    相关资源
    最近更新 更多