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

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


当前回答

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

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

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

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

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

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

其他回答

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

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

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

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

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

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

需要澄清的是,不仅数据应该是缓存友好的,它对代码也同样重要。这除了分支预处理、指令重新排序、避免实际除法和其他技术之外。

通常,代码越密集,存储它所需的缓存线就越少。这会导致更多缓存线可用于数据。

代码不应该到处调用函数,因为它们通常需要一个或多个自己的缓存线,从而导致数据的缓存线更少。

函数应该从缓存行对齐友好地址开始。尽管有(gcc)编译器开关,但要注意,如果函数很短,那么每个函数占用整个缓存线可能会很浪费。例如,如果三个最常用的函数放在一个64字节的缓存行中,那么这比每个函数都有自己的缓存行时浪费更少,并且导致两个缓存行不可用于其他用途。典型的对齐值可以是32或16。

所以,花一些额外的时间让代码更密集。测试不同的构造,编译并检查生成的代码大小和配置文件。

除了@Marc Claesen的答案之外,我认为缓存不友好代码的一个有启发性的经典例子是按列而不是按行扫描C二维数组(例如位图图像)的代码。

在一行中相邻的元素在内存中也是相邻的,因此按顺序访问它们意味着按升序访问它们;这是缓存友好的,因为缓存倾向于预取连续的内存块。

相反,按列访问这样的元素对缓存不友好,因为同一列上的元素在内存中彼此相距很远(特别是,它们的距离等于行的大小),所以当您使用这种访问模式时,您在内存中跳跃,可能会浪费缓存检索内存中附近元素的努力。

而破坏表演所需的一切就是

// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
    for(unsigned int x=0; x<width; ++x)
    {
        ... image[y][x] ...
    }
}

to

// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
    for(unsigned int y=0; y<height; ++y)
    {
        ... image[y][x] ...
    }
}

在具有小缓存和/或使用大阵列的系统中(例如,当前机器上的10+百万像素24bpp图像),这种效果可能非常显著(速度上有几个数量级);因此,如果您必须进行多次垂直扫描,通常最好先将图像旋转90度,然后再执行各种分析,将缓存不友好的代码限制在旋转范围内。

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

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

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

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

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

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