从寄存器移动到频繁访问的变量时性能意外降低

Ste*_*Mai 3 c assembly caching x86-64 perf

我正在使用以下示例了解缓存的工作原理:

\n
#include <stdio.h>\n#include <stdint.h>\n#include <stdlib.h>\n\ntypedef uint32_t data_t;\nconst int U = 10000000;   // size of the array. 10 million vals ~= 40MB\nconst int N = 100000000;  // number of searches to perform\n\nint main() {\n  data_t* data = (data_t*) malloc(U * sizeof(data_t));\n  if (data == NULL) {\n    free(data);\n    printf("Error: not enough memory\\n");\n    exit(-1);\n  }\n\n  // fill up the array with sequential (sorted) values.\n  int i;\n  for (i = 0; i < U; i++) {\n    data[i] = i;\n  }\n\n  printf("Allocated array of size %d\\n", U);\n  printf("Summing %d random values...\\n", N);\n\n  data_t val = 0;\n  data_t seed = 42;\n  for (i = 0; i < N; i++) {\n    int l = rand_r(&seed) % U;\n    val = (val + data[l]);\n  }\n\n  free(data);\n  printf("Done. Value = %d\\n", val);\n  return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

使用 perf record ./sum 找到的慢速随机访问循环的相关注释是

\n
  0.05 \xe2\x94\x82       mov    -0x18(%rbp),%eax                                                                 \xe2\x96\x92\n  0.07 \xe2\x94\x82       mov    -0x10(%rbp),%rcx                                                                 \xe2\x96\x92\n       \xe2\x94\x82       movslq -0x20(%rbp),%rdx                                                                 \xe2\x96\x92\n  0.03 \xe2\x94\x82       add    (%rcx,%rdx,4),%eax                                                               \xe2\x96\x92\n 95.39 \xe2\x94\x82       mov    %eax,-0x18(%rbp)                                                                 \xe2\x96\x92\n  1.34 \xe2\x94\x82       mov    -0x14(%rbp),%eax                                                                 \xe2\x96\x92\n       \xe2\x94\x82       add    $0x1,%eax                                                                        \xe2\x97\x86\n       \xe2\x94\x82       mov    %eax,-0x14(%rbp)\n
Run Code Online (Sandbox Code Playgroud)\n

此时,-0x18持有val-0x10持有data-0x14持有i-0x20持有l。左列的数字显示该指令所花费的时间百分比。我预计该行\nadd (%rcx, %rdx, 4), %eax会占用最多时间,因为它必须执行随机访问负载data[l](这只是(%rcx, %rdx, 4))。这应该只在大约 16k/U = 0.16% 的时间出现在 L1 缓存中,因为我的 L1 缓存大小为 64k 字节或 16k 整数。所以这个操作应该很慢。相反,显然很慢的操作只是从经常使用的寄存器移动%eaxval它肯定在高速缓存中。谁能解释一下发生了什么事吗?

\n

Pet*_*des 5

硬件性能计数器通常“归咎于”等待缓慢结果的指令( )store,而不是缓慢产生结果的指令。add(微融合到加载+添加微指令中的 内存源)。

和/或无论数据依赖性如何,它们都会在缓存未命中加载后归咎于下一条指令。这称为“打滑”或“歪斜”。例如,请参阅https://easyperf.net/blog/2018/08/29/Understanding-performance-events-skidhttps://www.brendangregg.com/perf.html

我对造成这种影响的假设是,我认为 Intel CPU 在引发中断时等待 ROB 中最旧的指令退出,也许是为了避免在高中断情况下使主线程挨饿。对于最终导致无序执行停止的缓存未命中加载,它将是 ROB 中最旧的,在加载数据到达之前无法退出(因为 x86 的强有序内存模型不会让加载在此之前退出) ,即使它们被认为是无故障的,与 ARM 不同)。因此,当“循环”事件的计数器下降到零并触发样本时,高速缓存未命中加载就会退出,程序顺序中的下一条指令将受到该样本的“指责”。

对于旨在附加到特定指令的事件,例如mem_load_retired.l3_miss,滑动会产生更多问题,但英特尔 PEBS 避免了这种情况。在上一段中,我讨论了“cycles”事件,该事件在停滞时每个周期都会滴答作响,但您可能会得到相同的事件,mem_load_retired.l3_miss直到从 L3 切片收到回音后才能检测到该事件。

在不停止的代码中,一组指令中在同一周期内全部退出的第一条或第二条指令可能会受到指责。CPU 必须从管道中正在运行的所有指令中选择一条指令来负责。无论是在何处引发中断(非 PEBS),还是哪个指令地址进入 PEBS 缓冲区。


另请参阅不一致的“perf annotate”内存加载/存储时间报告,这是一个不太简单/明显的情况,但归咎于等待缓慢结果的指令是其中的关键部分。