如何使用 C++ 代码验证 CPU 缓存行大小?

Jun*_* Ge 3 c c++ caching operating-system

我读了Igor 博客上的一篇文章。文章说:

\n\n
\n

...如今\xe2\x80\x99s CPU 不会逐字节访问内存。相反,它们以(通常)64 字节的块形式获取内存,称为缓存行。当您读取特定内存位置时,整个缓存行将从主内存提取到缓存中。而且,从同一缓存行访问其他值很便宜!

\n
\n\n

文章还提供了c#代码来验证上述结论:

\n\n
int[] arr = new int[64 * 1024 * 1024];\n\n// Loop 1 (step = 1)\nfor (int i = 0; i < arr.Length; i++) arr[i] *= 3;\n\n// Loop 2 (step = 16)\nfor (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;\n
Run Code Online (Sandbox Code Playgroud)\n\n

两个 for 循环花费的时间大约相同:在 Igor 的机器上分别为 80 毫秒和 78 毫秒,因此验证了缓存行机制。

\n\n

然后我参考上面的想法实现一个c++版本来验证缓存行大小,如下所示:

\n\n
#include "stdafx.h"\n#include <iostream>\n#include <chrono>\n#include <math.h>\nusing namespace std::chrono;\n\nconst int total_buff_count = 16;\nconst int buff_size = 32 * 1024 * 1024;\n\nint testCacheHit(int * pBuffer, int size, int step)\n{\n    int result = 0;\n    for (int i = 0; i < size;) {\n        result += pBuffer[i];\n        i += step;\n    }\n\n    return result;\n}\n\nint main()\n{\n    int * pBuffer = new int[buff_size*total_buff_count];\n\n    for (int i = 0; i < total_buff_count; ++i) {\n        int step = (int)pow(2, i);\n\n        auto start = std::chrono::system_clock::now();\n        volatile int result = testCacheHit(pBuffer + buff_size*i, buff_size, step);\n        auto end = std::chrono::system_clock::now();\n\n        std::chrono::duration<double> elapsed_seconds = end - start;\n        std::cout << "step: " << step << ", elapsed time: " << elapsed_seconds.count() * 1000 << "ms\\n";\n    }\n\n    delete[] pBuffer;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

但我的测试结果与Igor文章中的结果完全不同。如果步长为1,那么时间成本约为114ms;如果步长为16,那么时间成本约为78ms。测试应用程序是使用发布配置构建的,我的机器上有 32 GB 内存,CPU 是 intel Xeon E5 2420 v2 2.2G;结果如下。\nC++测试结果

\n\n

有趣的发现是,当步骤为 2 和步骤为 2048 时,时间成本显着降低。我的问题是,如何解释我的测试中步骤为 2 和步骤 2048 时的差距?为什么我的结果与 Igor 的结果完全不同?谢谢。

\n\n

我自己对第一个问题的解释是,代码的时间成本包含两部分:一个是“内存读/写”,其中包含内存读/写时间成本,另一个是“其他成本”,其中包含for循环和计算成本。如果步长为2,那么“内存读/写”成本几乎不会改变(因为缓存行),但计算和循环成本减少了一半,所以我们看到了明显的差距。我猜我的 CPU 上的缓存行是 4096 字节(1024 * 4 字节)而不是 64 字节,这就是为什么当步骤为 2048 时我们得到另一个间隙。但这只是我的猜测。感谢你们的任何帮助,谢谢。

\n

Dan*_*ica 5

下降 1024 至 2048

请注意,您使用的是未初始化的数组。这基本上意味着

int * pBuffer = new int[buff_size*total_buff_count];
Run Code Online (Sandbox Code Playgroud)

不会导致您的程序实际请求任何物理内存。相反,仅保留一些虚拟地址空间。

然后,当您第一次接触某个数组元素时,就会触发页面错误,操作系统将页面映射到物理内存。这是一个相对较慢的操作,可能会严重影响您的实验。由于系统上的页面大小可能为4 kB,因此它可以容纳1024 个 4 字节整数。当您选择2048 step时,实际上只有每隔一个页面被访问一次,并且运行时间会成比例下降。

你可以通过提前“接触”内存来避免这种机制的负面影响:

int * pBuffer = new int[buff_size*total_buff_count]{};
Run Code Online (Sandbox Code Playgroud)

当我尝试这样做时,在 64 到 8192 步长之间,时间几乎呈线性减少。

下降到 1 到 2 之间

你的系统上的缓存行大小绝对不是2048字节,很可能是64字节(通常,它可能有不同的值,甚至不同缓存级别的值也不同)。

至于第一部分,如果step是 1,则涉及更多的算术运算(数组元素的加法和 的增量i)。

与伊戈尔实验的区别

我们只能推测为什么伊戈尔的实验在两种情况下给出的时间几乎相同。我猜算术的运行时间可以忽略不计,因为只涉及一个循环计数器增量,并且他写入数组,这需要将缓存行额外传输回内存。(我们可以说字节/操作比率比您的实验中高得多。)