【问题标题】:How to verify CPU cache line size with c++ code?如何使用 C++ 代码验证 CPU 缓存行大小?
【发布时间】:2020-02-03 10:55:46
【问题描述】:

我读了一篇来自Igor's blog 的文章。文章说:

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

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

int[] arr = new int[64 * 1024 * 1024];

// Loop 1 (step = 1)
for (int i = 0; i < arr.Length; i++) arr[i] *= 3;

// Loop 2 (step = 16)
for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;

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

然后我参考上面的思路实现了一个c++版本来验证缓存行大小如下:

#include "stdafx.h"
#include <iostream>
#include <chrono>
#include <math.h>
using namespace std::chrono;

const int total_buff_count = 16;
const int buff_size = 32 * 1024 * 1024;

int testCacheHit(int * pBuffer, int size, int step)
{
    int result = 0;
    for (int i = 0; i < size;) {
        result += pBuffer[i];
        i += step;
    }

    return result;
}

int main()
{
    int * pBuffer = new int[buff_size*total_buff_count];

    for (int i = 0; i < total_buff_count; ++i) {
        int step = (int)pow(2, i);

        auto start = std::chrono::system_clock::now();
        volatile int result = testCacheHit(pBuffer + buff_size*i, buff_size, step);
        auto end = std::chrono::system_clock::now();

        std::chrono::duration<double> elapsed_seconds = end - start;
        std::cout << "step: " << step << ", elapsed time: " << elapsed_seconds.count() * 1000 << "ms\n";
    }

    delete[] pBuffer;
}

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

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

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

【问题讨论】:

  • stdafx.h 不是标准的 C++ 标头。见this
  • stdafx.h 是项目本身的头文件(用于预编译头文件),所以它当然不是标准头文件
  • ints 在大多数系统上是四个字节。

标签: c++ c caching operating-system


【解决方案1】:

介于 1024 和 2048 之间

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

int * pBuffer = new int[buff_size*total_buff_count];

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

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

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

int * pBuffer = new int[buff_size*total_buff_count]{};

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

介于 1 和 2 之间

您系统上的缓存线大小肯定不是 2048 字节,很可能是 64 字节(通常,它可能有不同的值,甚至对于不同的缓存级别可能有不同的值)。

至于第一部分,step 为 1,只涉及更多的算术运算(数组元素的添加和i 的增量)。

与伊戈尔的实验不同

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

【讨论】:

    【解决方案2】:

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

    C++17 中有std::hardware_destructive_interference_size,它应该提供最小的缓存行大小。请注意,它是一个编译时间值,编译器依赖于您对目标机器的输入。当针对整个架构时,数字可能不准确。

    【讨论】:

      【解决方案3】:

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

      你确实不能。

      您应该编写portable C++ 代码。阅读n3337

      假设您没有在 C++ 编译器中启用 compiler optimizations。想象一下你在某个模拟器中运行你的 C++ 编译器(比如these)。

      特别是在 Linux 上,您可以解析 /proc/cpuinfo 伪文件并从中获取 CPU 缓存行大小。

      例如:

      % head -20 /proc/cpuinfo
      processor   : 0
      vendor_id   : AuthenticAMD
      cpu family  : 23
      model       : 8
      model name  : AMD Ryzen Threadripper 2970WX 24-Core Processor
      stepping    : 2
      microcode   : 0x800820b
      cpu MHz     : 1776.031
      cache size  : 512 KB
      physical id : 0
      siblings    : 48
      core id     : 0
      cpu cores   : 24
      apicid      : 0
      initial apicid  : 0
      fpu     : yes
      fpu_exception   : yes
      cpuid level : 13
      wp      : yes
      flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid amd_dcm aperfmperf pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb hw_pstate sme ssbd sev ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 xsaves clzero irperf xsaveerptr arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif overflow_recov succor smca
      

      顺便说一句,缓存有许多不同的组织和级别。

      您可以想象 Linux 上的 C++ 应用程序解析 /proc/cpuinfo 的输出,然后向 Web 发出 HTTP 请求(使用 libcurl)以从中获取更多信息。

      另请参阅this 答案。

      【讨论】:

      • @JunGe 另请注意,不同缓存级别的缓存行大小通常可能不同。
      • @DanielLangr 有趣的是,AMD 定义了 CPUID 叶 0x80000006,因此它只有一个缓存行大小字段。非 AMD 缓存信息将缓存级别作为参数所以它可以为不同的级别提供不同的线条尺寸
      猜你喜欢
      • 2012-04-28
      • 2016-06-25
      • 2017-12-18
      • 1970-01-01
      • 2021-11-27
      • 1970-01-01
      • 2017-07-09
      • 1970-01-01
      • 2013-07-05
      相关资源
      最近更新 更多