处理器堆读取和预取

Processor heap read and prefetch

所以我试图弄清楚堆读取如何可能通过预取来减慢处理器的性能,这只是理论上的问题,所以在示例中我使用了一些类似于 C 的神奇语言。

所以让我们假设我们有一些 120 字节的堆,并且它有一些内存被程序使用。 [0...19, /* 免费 /, 40-79, / 免费直到最后 (119) */] 我有一些结构与 20 字节的内存神奇对齐

#include <stdlib.h>

struct magic_struct {
   long long int foo[3];
   short int bar;
};

typedef MagicStruct struct magic_struct;

void read_magic_struct(MagicStruct* buzz) {
   // Some code to read struct
} 

int main(void) {
   MagicStruct *str1 = malloc(sizeof(MagicStruct));
   MagicStruct *str2 = malloc(sizeof(MagicStruct));

   read_magic_struct(str1);
   read_magic_struct(str2);
   
   free(str1);
   free(str2);
}

所以让我们假设我们的处理器获取 40 字节的缓存行, 这意味着我们当前的内存表示处理器无法在读取 str1 时预取 str2,因此它会减慢程序执行速度?如果有空内存缓冲区或第一个空内存块将是 40 字节,如何分配结构?如果结构的大小为 50 字节,处理器是否会命中缓存未命中?是否有某种机制决定何时何地在堆上分配内存?

Does some mechanism decides where and when allocate memory on heap?

决定内存在堆上分配位置的机制是一段称为“内存分配器”的代码——C 运行time 库的一部分——它不太关心预取或诸如此类的事情。大多数内存分配器尽最大努力使“相关”分配“靠近在一起”,但这只是在尽力而为的基础上。所以你不能真正假设这两个分配有多远。

How do structs get allocated if there was an empty memory buffer or first empty memory chunk would be 40 bytes along?

以任何可能的方式:只有阅读系统上 C 运行time 库使用的内存分配器的源代码才能知道(例如大多数 [=51= 上的 glibc) ] 系统)。这完全是任意的,通常您无法预测这些结构将如何分配。而且在现实生活中堆不必是连续的,即堆不必是一个大的内存块,它通常是许多内存块,它们之间有间隙。

So let's suppose that our processor fetches cacheline of 40 bytes, it means with our current memory representation processor can't prefetch str2 while reading str1 so it will be slow down in program execution?

假设您提出的堆布局,唯一有效的语句如下:包含str1的缓存行不包含str2。你能说的就这么多,仅此而已。它没有告诉您有关预取的任何信息,因为预取与在需要之前提前获取 other 缓存行有关。

我认为您误用了“预取”一词,因为处理器仅根据完整的高速缓存行与内存交换数据。预取并不意味着在获取单个缓存行时,其他一些有用的数据也在该缓存行内。我们称之为缓存一致性,它是关于您如何在程序中布置数据的 属性。如果你需要这么精细的控制,你需要自己写内存分配器(即使很简单)。

预取意味着处理器有一个系统可以监控正在获取的缓存行,并且预计需要另一个缓存行,并在您使用它之前获取它。预取可以由显式预取机器指令触发,如果你想将它控制到这样的程度,或者它可以由处理器中实现的预取算法触发。现代处理器非常擅长检测顺序访问,这些访问是连续的,甚至被重复的间隙分开。


思考这样的事情当然很好,但你必须明白处理器非常好,如果你认为预取可能是你的问题,你必须有一些很好的论据为什么会这样,并且通常你需要实际测量。在没有测量表明处理器正在等待缓存行从内存进入的情况下谈论优化预取绝对是愚蠢的,也是浪费时间。因此,实际上,如果您想在真实处理器上使用真实预取来探索这一点(而不是虚构的东西),则必须安装例如Intel VTune Profiler,或使用 Valgrind 的 cachegrind(在 linux 上),运行 你的程序在这些工具下,然后能够解释结果,查明问题,通过更改数据布局或使用显式预取机器来解决它们命令(或编译器内部指令),然后能够通过查看改进的缓存命中率来验证您的解决方案。理想情况下,这应该是自动化的,这样你就不会出现性能退化,即你会 运行 所有这些检测和结果解释作为持续集成系统下的脚本,这样你就知道你不会不小心破坏东西.但这需要 很多 的工作,如果您是从头开始(一个新项目),假设您至少精通脚本、CI 系统和 C,那么它仍然可能需要数百小时才能完全设置并摇摇欲坠才能值得信赖。指导原则是:

缺乏衡量是您不关心结果的初步证据

换句话说,如果您确实关心表现的某些方面,那么表现这种关心的唯一方法就是衡量它,并且让这些衡量成为其中的一部分正常的工作流程,即一旦它们被设置,你甚至不用担心它们——你签入一些新代码,如果你破坏了性能,性能测试将失败,你必须在你之前修复它'允许合并。这就是它通常在重要的环境中完成的方式。否则,唯一的解释是您 不能 关心,因为如果没有自动测量,几乎可以肯定一些“小”代码更改会影响性能,但会被忽视。一旦你被告知,就没有回头路了,抱歉:)