【问题标题】:Confusing Caching Behaviour of a Simple C Program简单 C 程序的令人困惑的缓存行为
【发布时间】:2018-05-28 11:37:39
【问题描述】:

我正在试验一个程序,看看它的缓存行为是否与我的概念理解一致。

为此,我使用 Perf 命令:

perf stat -e cache-misses ./a.out

记录以下简单C程序的缓存未命中率:

int main() {
    int N = 10000;
    double *arr = malloc(sizeof(double) * N * N);

    for(int i = 0; i < N; i++) {
        for(int j = 0; j < N; j++){
            arr[i * N + j] = 10.0;
        }
    }
    return 0;
}

我的缓存未命中率为 50.212%。如果我按如下方式更改数组访问模式:

arr[j * N + i]

我知道缓存未命中率为 22.206%。

这些结果让我感到惊讶。

  1. 50.212% 的缓存未命中率对于这样一个具有非常规则内存访问模式的简单程序来说似乎非常高。我希望这更接近 1/(num-words-per-cache-line) 肯定大于 1/2。为什么缓存未命中率这么高?
  2. 我对内存的(有限)理解表明,以列优先顺序遍历数组应该会导致更糟糕的缓存行为,但我得到的结果却恰恰相反。发生了什么事?

【问题讨论】:

  • 你有什么CPU? cat /proc/cpuinfo 也会告诉你缓存信息。
  • @JonathonReinhart 它是 Intel(R) Xeon(R) CPU,缓存大小为 12288 KB。
  • 您生成的程序是否启用了优化?
  • 出于好奇,double (*arr)[N] = malloc( sizeof(double[N][N]) ); ... arr[i][j] = 10.0 会生成更快的代码吗?
  • 请显示为两个源生成的程序集(GCC 的-S 开关)和您使用的所有编译器开关。 (必须没有启用优化,因为这个程序相当于int main() {}。)perf 是从main 的开头开始计数,还是在C 运行时启动代码中包含初始化?是否包括指令缓存未命中?

标签: c performance caching memory perf


【解决方案1】:

答案很简单:编译器优化你的分配。下面是反汇编代码的样子:

10  int main() {
   0x00000000004004d6 <+0>: mov    $0x2710,%edx
   0x00000000004004db <+5>: jmp    0x4004e7 <main+17>

15          for(int j = 0; j < N; j++){
   0x00000000004004dd <+7>: sub    $0x1,%eax
   0x00000000004004e0 <+10>:    jne    0x4004dd <main+7>

14      for(int i = 0; i < N; i++) {
   0x00000000004004e2 <+12>:    sub    $0x1,%edx
   0x00000000004004e5 <+15>:    je     0x4004ee <main+24>

10  int main() {
   0x00000000004004e7 <+17>:    mov    $0x2710,%eax
   0x00000000004004ec <+22>:    jmp    0x4004dd <main+7>

16              arr[i * N + j] = 10.0;
17          }
18      }
19      return 0;
20  }
   0x00000000004004ee <+24>:    mov    $0x0,%eax
   0x00000000004004f3 <+29>:    retq   

如您所见,没有为行 arr[i * N + j] = 10.0; 生成汇编指令,因此您观察到的那些缓存未命中与 perf 无关。

修复很简单。只需在指针声明中添加volatile,强制编译器生成赋值,即:

volatile double *arr = malloc(sizeof(double) * N * N);

现在反汇编:

10  int main() {
   0x0000000000400526 <+0>: sub    $0x8,%rsp

11      int N = 10000;
12      volatile double *arr = malloc(sizeof(double) * N * N);
   0x000000000040052a <+4>: mov    $0x2faf0800,%edi
   0x000000000040052f <+9>: callq  0x400410 <malloc@plt>
   0x0000000000400534 <+14>:    mov    $0x0,%edx

16              arr[i * N + j] = 10.0;
   0x0000000000400539 <+19>:    movsd  0xc7(%rip),%xmm0        # 0x400608
   0x0000000000400541 <+27>:    jmp    0x40055f <main+57>
   0x0000000000400543 <+29>:    movslq %edx,%rcx
   0x0000000000400546 <+32>:    lea    (%rax,%rcx,8),%rcx
   0x000000000040054a <+36>:    movsd  %xmm0,(%rcx)
   0x000000000040054e <+40>:    add    $0x1,%edx

15          for(int j = 0; j < N; j++){
   0x0000000000400551 <+43>:    cmp    %esi,%edx
   0x0000000000400553 <+45>:    jne    0x400543 <main+29>
   0x0000000000400555 <+47>:    mov    %esi,%edx

14      for(int i = 0; i < N; i++) {
   0x0000000000400557 <+49>:    cmp    $0x5f5e100,%esi
   0x000000000040055d <+55>:    je     0x400567 <main+65>
   0x000000000040055f <+57>:    lea    0x2710(%rdx),%esi
   0x0000000000400565 <+63>:    jmp    0x400543 <main+29>

17          }
18      }
19      return 0;
20  }
   0x0000000000400567 <+65>:    mov    $0x0,%eax
   0x000000000040056c <+70>:    add    $0x8,%rsp
   0x0000000000400570 <+74>:    retq   

还有更多的缓存未命中。

只运行一次测试可能会得到非常嘈杂的结果。您应该进行几次测量并取一个中值。有基准框架,例如 Google Benchmark,所以请看一下。

最后一个。您的两种模式,如 i * N + jj * N + i 都很容易被 CPU 预取器识别,因此两种情况下的缓存未命中率应该非常相似......

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-11
    • 1970-01-01
    • 2017-12-05
    • 2011-11-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多