“缓存友好代码”和“缓存友好”代码之间有什么区别?

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


当前回答

缓存友好代码是经过优化以有效利用CPU缓存的代码。这通常涉及以利用空间和时间局部性的方式组织数据,这是指一起访问的数据可能一起存储在内存中,并且频繁访问的数据很可能在不久的将来再次访问。

有几种方法可以使代码缓存友好,包括:

使用连续内存布局:通过在连续内存中存储数据块,可以利用空间位置和减少缓存未命中的数量。使用数组:当您需要按顺序访问数据,因为它们允许您利用时间局部性,并将热数据保存在缓存中。小心使用指针:指针可用于访问不连续存储在内存中,但它们也会导致如果过度使用,缓存将丢失。如果您需要使用指针,尝试以利用空间的方式使用它们以及时间局部性,以最小化缓存未命中。使用编译器优化标志:大多数编译器都有优化可用于优化CPU缓存使用的标志。这些标志可以帮助减少缓存未命中的数量代码的总体性能。

需要注意的是,最适合优化CPU缓存使用的特定技术将取决于系统的特定要求和约束。可能需要尝试不同的方法,以找到满足您需求的最佳解决方案。

其他回答

请注意,缓存不只是缓存连续内存。它们有多行(至少4行),因此不连续和重叠的记忆通常可以同样有效地存储。

以上所有示例中缺少的是衡量基准。关于表演有很多神话。除非你测量它,否则你不知道。除非有明显的改进,否则不要使代码复杂化。

欢迎来到面向数据设计的世界。基本的口头禅是“排序”、“消除分支”、“批处理”和“消除虚拟呼叫”,所有这些步骤都是为了更好地定位。

既然你用C++标记了这个问题,这里是强制性的典型C++废话。托尼·阿尔布雷希特(Tony Albrecht)的《面向对象编程的陷阱》也是对这一主题的一个很好的介绍。

简单来说:缓存不友好代码与缓存友好代码的典型例子是矩阵乘法的“缓存阻塞”。

朴素矩阵乘法看起来像:

for(i=0;i<N;i++) {
   for(j=0;j<N;j++) {
      dest[i][j] = 0;
      for( k=0;k<N;k++) {
         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=0;k<N;k++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

如果缓存行大小为64字节,并且我们使用32位(4字节)浮点运算,那么每个缓存行有16个项目。仅通过这种简单的转换,缓存未命中的数量就减少了大约16倍。

Fancier变换对2D平铺进行操作,优化多个缓存(L1、L2、TLB),等等。

谷歌搜索“缓存阻塞”的一些结果:

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

缓存友好代码是经过优化以有效利用CPU缓存的代码。这通常涉及以利用空间和时间局部性的方式组织数据,这是指一起访问的数据可能一起存储在内存中,并且频繁访问的数据很可能在不久的将来再次访问。

有几种方法可以使代码缓存友好,包括:

使用连续内存布局:通过在连续内存中存储数据块,可以利用空间位置和减少缓存未命中的数量。使用数组:当您需要按顺序访问数据,因为它们允许您利用时间局部性,并将热数据保存在缓存中。小心使用指针:指针可用于访问不连续存储在内存中,但它们也会导致如果过度使用,缓存将丢失。如果您需要使用指针,尝试以利用空间的方式使用它们以及时间局部性,以最小化缓存未命中。使用编译器优化标志:大多数编译器都有优化可用于优化CPU缓存使用的标志。这些标志可以帮助减少缓存未命中的数量代码的总体性能。

需要注意的是,最适合优化CPU缓存使用的特定技术将取决于系统的特定要求和约束。可能需要尝试不同的方法,以找到满足您需求的最佳解决方案。

今天的处理器可以处理许多级别的级联内存区域。因此,CPU芯片上会有一堆内存。它可以快速访问此内存。有不同级别的缓存,每次访问速度都比下一次慢(并且更大),直到您到达不在CPU上且访问速度相对较慢的系统内存。

从逻辑上讲,对于CPU的指令集,您只需要引用一个巨大的虚拟地址空间中的内存地址。当你访问一个单独的内存地址时,CPU会去获取它。在过去,它只会获取那个单独的地址。但今天,CPU将在您请求的位周围获取一堆内存,并将其复制到缓存中。它假设如果你要求一个特定的地址,那么你很可能很快就会要求附近的地址。例如,如果您正在复制缓冲区,您将从连续地址读取和写入数据—一个紧接着另一个。

所以今天当你获取一个地址时,它会检查第一级缓存,看看它是否已经将该地址读取到缓存中,如果它没有找到它,那么这就是缓存未命中,它必须进入下一级缓存才能找到它,直到它最终进入主内存。

缓存友好的代码试图使访问在内存中保持紧密,以便最大限度地减少缓存未命中。

所以一个例子是假设你想要复制一个巨大的二维表格。它在内存中以连续的到达行组织,一行紧接着下一行。

如果您从左到右一次复制一行元素,这将是缓存友好的。如果您决定一次复制一列表,您将复制完全相同的内存量,但这将不利于缓存。