有人可能能够"缓存友好代码"的一个示例,该代码的"缓存友好"版本?

如何确保我写缓存高效的代码?

2013-05-22 18:37:01
问题评论:

这可能给您点提示︰ stackoverflow.com/questions/9936132/...

此外应注意的缓存行大小。在现代处理器上,它通常是 64 个字节。

这里是另一个很好的文章。这些原则适用于在任何操作系统 (Linux、 MaxOS 或 Windows) 的 C/c + + 程序︰ lwn.net/Articles/255364

相关的问题︰ stackoverflow.com/questions/8469427/...

stackoverflow.com/questions/763262/...

回答:

预备知识

在现代的计算机上只有最低级别的内存结构 (注册) 可以移动数据在单个时钟周期。不过,寄存器非常昂贵,大多数计算机内核具有小于几个 dozen 注册 (几百个到也许千总字节数)。是 (DRAM) 内存彩虹的另一端,内存开销非常小 (即按其原义数以百万计的更便宜的次数) 但需要数百个周期后接收数据的请求。桥接 super 快速和便宜和超级慢而廉价之间的缺口是缓存记忆,减少速度和成本中的名为 L1、 L2 和 L3。其思想是代码的,大部分执行将会碰到少数变量通常情况下和其他 (更大的一组变量) 的很少。如果处理器不能在 L1 缓存中查找数据,然后,它查找 L2 高速缓存中。如果不存在,然后 L3 高速缓存,并且如果不存在,主内存。每一个这些"失误"的是花很多时间。

(比喻这种情况是高速缓存到系统内存,系统内存是为硬盘存储。硬磁盘存储是超级便宜,但速度很慢)。

缓存是一种主要的方法,以减少延迟(cfr) 等 Sutter 讲话开始时链接的影响。来解答问题等 Sutter (cfr。 下面链接)︰增加的带宽很容易,但我们不能购买我们摆脱滞后时间.

数据始终通过内存层次结构 (最小 = = 最快到最慢)。缓存命中未命中通常指的是最高的 cpu 的缓存命中/小姐-按最高级别我的意思是最大最慢 = =。缓存命中率至关重要的性能,因为每个缓存不命中结果中提取数据从 RAM (或更糟...) 采用了很多的时间 (数以百计的 RAM,数千万的 HDD 的周期的周期)。在比较中,从 (最高级别) 读取数据缓存通常要花费只有少数的周期。

在现代计算机体系结构、 性能瓶颈正在离开 CPU 芯片 (例如访问 RAM 或更高版本)。这只会更糟随着时间的推移。处理器频率的增加不再当前相关来提高性能。问题是内存访问。在 Cpu 的硬件设计工作因此当前侧重点是优化缓存、 预取、 管道和并发。例如,现代 Cpu 大约花费 85%的片上高速缓存和达 99%,存储/移动数据的 !

没有了很多次要说的主题。这里有几个好引用有关缓存、 内存层次结构和适当的编程︰

对于缓存友好代码的主要概念

高速缓存友好代码的非常重要的方面是所在地的关乎原则,它的目标是所在地的将关闭相关的数据放入内存以允许有效的缓存。根据 CPU 高速缓存,值得观注缓存行,要理解这是如何工作的︰缓存行是如何工作?

以下特定方面有高的重要性,以优化缓存︰

  1. 时间地点︰ 当访问给定的内存位置时,很可能在不久的将来再次访问时的相同位置。理想情况下,这些信息仍将缓存在该点。
  2. 空间的位置︰ 这是指将相互接近的相关的数据。在许多级别,而不仅仅是在 CPU 上缓存响应。例如,当您读取从 RAM,通常更大的内存块提取比什么特别要求因为程序经常会很快需要该数据。HDD 缓存按照同一行的想法。专门为 CPU 缓存的缓存行的概念非常重要。

使用适当容器

一个简单的示例缓存友好的而不是缓存友好是std::liststd::vectorstd::vector的元素存储在连续的内存,且这种情况下访问这些多缓存友好访问std::list,它存储了它四处走动的内容中的元素相比。这是因为空间的局部性。

Bjarne Stroustrup此 youtube 剪辑(感谢 @Mohammad 的链接的阿里 Baydoun !) 中提供的非常好的展示。

不要忽视的缓存中的数据结构和算法设计

只要有可能,请尝试调整您的数据结构和允许使用最大缓存的方式计算的顺序。常用的技术在这方面是阻止缓存 (Archive.org 版),它在高性能计算 (cfr。 例如ATLAS是极端重要的).

了解并利用隐式数据结构

另一个简单的示例,在该字段中的许多人有时会忘记被列为主 (例如) 与行主要用于存储二维数组排序 (例如)。例如,请考虑以下矩阵︰

1 2
3 4

在行优先排序,这存储在内存中为1 2 3 4列主要订购这会1 3 2 44 存储。很容易地实现后者不利用这种排序快速将遇到 (很容易地避免 !) 高速缓存问题,请参阅。遗憾的是,我看到与此非常类似的东西经常在我的域 (机器学习)。@MatteoItalia 在他的回答详细介绍此示例。

当从内存中取一个矩阵的某些元素,将提取以及附近的元素,并存储中的缓存行。如果排序被利用,这将导致更少的内存访问,(因为中的缓存行已经是下一步的几个值所需要的后续计算)。

为简单起见,假设缓存包含单个缓存行可以包含 2 个矩阵元素和,当某个给定的元素即从内存中提取出来下, 一个会过。说我们想要接管上述示例 2x2 矩阵中所有元素之和 (让它调用M):

利用 (例如更改列索引第一次在中的排序):

M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses

不利用 (如更改行索引第一次在中的排序):

M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses

在此简单示例中,利用订购大约两倍的执行速度 (因为内存访问,需要更多周期比计算总和)。在实践中的性能差异可能是大。

避免产生不可预知的分支

现代体系结构特征的管道和编译器变得擅于重新排序代码最小化由于内存访问延迟。当关键代码包含 (不可预测的) 的分支机构时,很难或无法预取数据。这间接地将导致更多的缓存未命中数。

这解释了是非常这里 (感谢 @0x90 的链接):为什么是它更快地处理比未排序的数组的已排序的数组?

避免虚函数

的上下文中,virtual方法表示缓存未命中数 (普遍的观点存在,它们可能在性能方面时应避免使用) 就有争议问题。虚函数可以促使缓存未命中时的外观,但只有这种情况如果不经常调用的特定函数 (否则它将很可能被缓存),因此这将被视为非问题被一些。有关参考关于此问题,请查阅︰具有 c + + 类中的虚方法的性能开销是什么?

常见的问题

多处理器缓存现代体系结构中常见的问题称为伪共享这发生在每个单独的处理器尝试使用另一个内存区域中的数据时,尝试将其存储在相同的缓存线这会导致缓存行,其中包含另一个处理器可以使用-一次次地被覆盖的数据。实际上,不同的线程进行相互等待通过引入缓存未命中数,这种情况下。此外 (感谢 @Matt 的链接),请参阅︰如何以及何时与缓存行大小相一致?

差缓存在内存 (这可能不是什么意思在此上下文中) 的极端表现是所谓颠簸这发生在过程会不断地产生页面错误 (如不在当前页访问内存) 要求磁盘访问。

可能无法展开答案有点还解释,在多线程的代码数据也可以是太本地 (如伪共享)

可以有任意多个级别的缓存芯片设计思考是很有用。通常他们会平衡速度与大小。如果无法使 L1 缓存 L5,一样大,一样快,您只需要 L1。

我意识到安排空柱的协议在 StackOverflow 批准的但这是诚实地为止看到的清晰、 最佳,答案。出色的工作,Marc。

@JackAidley 感谢您,为您喝彩的 !我看到的关注量接收到这个问题,我意识到很多人可能会感兴趣的较广泛的解释。很高兴很有用。

您没有提到是友好的数据结构设计以适应缓存行,要更好地利用缓存线的内存对齐该缓存。但是回答卓越 !超。

除了 @Marc Claesen 的答案,我认为有益缓存友好代码的典型示例是扫描 C bidimensional 数组 (如位图图像) 按列而不是按行的代码。

相邻行中的元素也是在内存中,因此访问这些内存存储顺序; 按升序序列方法中访问这些相邻这是缓存友好,因为缓存往往要预取的连续内存块。

相反,按列访问这种元素是缓存不友好,因为在同一列上的元素在内存中彼此相距很远 (具体而言,他们距离等于行的大小),那么当您使用这种访问模式,围绕在内存中的跳转,可能浪费了检索元素附近的内存中的缓存的工作。

它采用的全部是从转,

for(unsigned int y=0; y<height; ++y)
{
    for(unsigned int x=0; x<width; ++x)
    {
        ... image[y][x] ...
    }
}

for(unsigned int x=0; x<width; ++y)
{
    for(unsigned int y=0; y<height; ++x)
    {
        ... image[y][x] ...
    }
}

此效果可以与大阵列 (例如 10 + 万像素 24 bpp 图像在当前计算机上); 小的高速缓存和/或工作的系统中是相当生动 (几个订单的小量的速度)出于此原因,如果您需要做很多垂直扫描,通常最好先旋转 90 度的图像和进行各种分析以后,限制高速缓存友好代码只向旋转。

@RafaelBaptista︰ 嗯,它经典:)

错误,应为 x < 宽度?

现代的图像编辑器使用麻将牌作为内部存储,如的 64 x 64 像素块。这是更多缓存友好的本地操作放置 dab (运行模糊滤镜) 相邻像素相近的内存在两个方向,因为大部分时间。

我已尝试使用计时一个类似的示例,我在机器上,然后我发现时间是相同。别人已经尝试计时它吗?

@I3arnon︰ 不对,第一个是缓存友好的因为通常在 C 数组存储在行优先的顺序 (当然如果由于某种原因您的映像存储在列优先的顺序反向是如此)。

很大程度上优化缓存使用情况汇集到两个因素。

引用位置

第一个因素 (哪些其他人都已提到) 是的引用地址。引用地址的真正虽然有两个维度︰ 空间和时间。

  • 空间

空间的维度还配到两件事︰ 首先,我们要紧密,打包我们的信息的详细信息将放入内存有限,所以。这意味着 (例如) 您需要计算的复杂性,以证明根据小节点加入通过指针的数据结构中的一个重大进步。

第二,我们希望将会一起处理的信息也位于组合在一起。典型的缓存工作"线",即︰ 当您访问某些信息、 附近地址的其他信息将加载到缓存中与我们接触到的部分。例如,当触摸一个字节,缓存可能会加载附近一个 128 或 256 个字节。要充分利用这一点,,您通常希望的数据排列,以最大化您还将使用在同一时间加载的其他数据的可能性。

例如刚刚确实微不足道,这可能意味着线性搜索,可以使用二进制搜索与您所预期得更具竞争力。一旦从缓存行,已载入一项使用其余的数据,因为几乎免费也是缓存行。二进制搜索明显快变成只有数据时大型二进制搜索可以减少缓存您访问的行数。

  • 时间

时间维度意味着做一些数据上的某些操作时,您希望 (尽可能) 同时进行对该数据的所有操作。

标记了此作为 c + +,因为我将指向缓存相对友好的设计的一个典型示例︰ std::valarrayvalarray重载大多数的算术运算符,以便 (例如),我可以说a = b + c + d;(其中abcd是所有 valarrays) 为 element-wise 增加这些阵列。

这样的问题是它遍历输入的一对,将结果放入一个临时,演示了另一对的输入,等等。用大量的数据,从一个计算的结果可能会消失从缓存之前它用在下一次的计算,所以我们最终读数 (和书写) 数据重复之前我们得到我们的最终结果。如果最终结果的每个元素将类似(a[n] + b[n]) * (c[n] + d[n]);,我们通常希望一次读取每个a[n]b[n]c[n]d[n] 、 执行计算、 写入结果、 递增n和重复致力于使我们就大功告成。2

共享行

第二个主要因素避免行共享。要理解这一点,我们可能需要备份并有点看缓存的组织方式。直接映射缓存的最简单的形式。这意味着只能在主内存中的一个地址存储在缓存中的一个特定位置。如果我们使用的两个映射到相同的作用点在缓存中的数据项目,它不起作用-每次我们使用一个数据项,另不得不为其他留在缓存被刷新。缓存的其余部分是空的但这些项目将不会使用缓存的其他部分。

若要防止出现这种情况,大多数缓存是所谓的"关联集"。例如,在 4 路集关联缓存中,从主内存的任何项目可以存储在任何缓存中 4 不同的地方。因此时将缓存加载项,它寻找那些四间最近最少使用的3项、 刷新它到主内存中,并加载它的位置中的新项。

该问题是可能相当明显︰ 为直接映射缓存,恰好映射到相同的缓存位置的两个操作数会导致错误行为。N-路设置关联缓存增加数从 2 到 N + 1。将缓存组织成多个"方法"花费额外电路和一般运行得较慢,这样 (例如) 8192 方式集关联的缓存也是很少会很好的解决方案。

最终,这一因素是更难控制可移植的代码不过。您对您的数据的放置位置的控制通常是相当有限的。更糟糕的是,到缓存的确切地址映射否则为类似处理器之间会有所不同。但是,在某些情况下,它可以是值得做这样的事情分配较大的缓冲区,并使用只有您所分配的部件为确保防止数据共享相同的缓存线 (尽管您可能需要检测精确处理器并采取相应的措施来执行此操作)。

  • 伪共享

还有其他的相关项目称为"伪共享"。这将产生在多处理器或多核系统中,其中两个 (或更多) 处理器/内核的数据是独立的但在同一降到缓存行。虽然每一个都具有其自己单独的数据项,这会迫使两个处理器/核心协调他们对数据的访问。尤其是这两个修改中替换的数据,这可能导致大规模的下降,因为数据处理器之间不断往返。这很容易不能通过将组织缓存到更多的"方法"或任何类似的或者被清除。阻止它的主要办法是保证两个线程很少 (最好是从不) 修改可能是在相同的缓存行 (使用同一告诫难以控制的数据分配的地址) 的数据。


  1. 熟悉 c + + 的人可能想知道是否这很容易导致通过表达式模板类似的优化。我很肯定的回答是,它未能完成,如果是,它很可能是相当重大的胜利。我并不知道的人具有执行此操作,但是,和给定valarray获取使用多么少,我愿意看到有人因此执行至少有点吃惊。

  2. 如果任何人想知道是不valarray (专为性能设计) 可能是这严重错误,它涉及到一件事︰ 像旧版本的 Crays,快速主内存和高速缓存使用的机器是真的。这真的是一个接近理想的设计。

  3. 的我要简化︰ 大多数缓存不真正准确地说,衡量最近最少使用的项,但它们使用的要接近,而不需要对每个访问完整的时间戳一些启发式算法。

我喜欢您的答案,特别是valarray示例中信息的额外部分。

最后一个 + 1︰ 普通组相关性的说明 !进一步编辑︰ 这是一个在北部资料最丰富的答案谢谢。

欢迎访问数据面向设计的世界。基本的要诀是排序,消除了分支,批处理,消除virtual调用的所有步骤,朝向更好的位置。

因为您标记使用 c + +,这里的问题典型的 c + + Bullshit强制性的。Tony Albrecht缺陷的对象面向编程也是插入主题的出色介绍。

您什么意思通过批处理,其中一个可能会不理解。

批处理︰ 而不是在单个对象上执行的工作单元,它对执行一批的对象。

也称为阻止、 拦截寄存器、 阻止缓存。

阻止/非阻塞通常是指在并发环境中对象的行为方式。

批处理 = =向量化

只指堆积︰ 缓存友好与缓存友好代码的典型示例是"缓存阻止"矩阵的乘法。

Naive 矩阵乘法如下所示

for(i=0;i<N;i++) {
   for(j=0;j<N;j++) {
      dest[i][j] = 0;
      for( k==;k<N;i++) {
         dest[i][j] += src1[i][k] * src2[k][j];
      }
   }
}

如果N较大,例如如果N * sizeof(elemType)是大于高速缓存大小,然后对每个单个访问src2[k][j]将缓存未命中。

有许多不同的方法来优化此缓存。下面是一个非常简单的例子︰ 而不是读取内部循环中的缓存行的每一项时,使用的所有项目︰

int itemsPerCacheLine = CacheLineSize / sizeof(elemType);

for(i=0;i<N;i++) {
   for(j=0;j<N;j += itemsPerCacheLine ) {
      for(jj=0;jj<itemsPerCacheLine; jj+) {
         dest[i][j+jj] = 0;
      }
      for( k==;k<N;i++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

如果缓存行大小为 64 个字节,并且我们只在浮点数 32 位 (4 字节) 上运行,则每个缓存行的 16 项。并通过就此简单转换缓存未命中数的减少了大约 16-fold。

更别致转换 2D 图块上运行、 优化的多个缓存 (L1、 L2、 TLB),等等。

"缓存拦截"googling 一些结果︰

http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf

http://software.intel.com/en-us/articles/cache-blocking-techniques

优化的缓存阻塞算法好的视频动画。

http://www.youtube.com/watch?v=IFWgwGMMrh0

密切相关循环平铺︰

http://en.wikipedia.org/wiki/Loop_tiling

这时候的人也可能感兴趣我关于矩阵乘法的文章我在何处测试通过矩阵乘以两个 2000 x 2000"缓存友好"ikj 算法和不友好的 ijk 算法。

内容来源于Stack Overflow What is “cache-friendly” code?
请输入您的翻译

What is “cache-friendly” code?

确认取消