“perf annotate”内存加载/存储时间报告不一致

rus*_*tyx 2 performance assembly x86-64 micro-optimization perf

我很难解释英特尔性能事件报告。

\n

考虑以下主要读/写内存的简单程序:

\n
#include <stdint.h>\n#include <stdio.h>\n\nvolatile uint32_t a;\nvolatile uint32_t b;\n\nint main() {\n  printf("&a=%p\\n&b=%p\\n", &a, &b);\n  for(size_t i = 0; i < 1000000000LL; i++) {\n    a ^= (uint32_t) i;\n    b += (uint32_t) i;\n    b ^= a;\n  }\n  return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我用gcc -O2以下命令编译它并运行perf

\n
#include <stdint.h>\n#include <stdio.h>\n\nvolatile uint32_t a;\nvolatile uint32_t b;\n\nint main() {\n  printf("&a=%p\\n&b=%p\\n", &a, &b);\n  for(size_t i = 0; i < 1000000000LL; i++) {\n    a ^= (uint32_t) i;\n    b += (uint32_t) i;\n    b ^= a;\n  }\n  return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

结果perf annotate(由我注释为内存加载/存储):

\n
# gcc -g -O2 a.c\n# perf stat -a ./a.out\n&a=0x55a4bcf5f038\n&b=0x55a4bcf5f034\n\n Performance counter stats for \'system wide\':\n\n         32,646.97 msec cpu-clock                 #   15.974 CPUs utilized\n               374      context-switches          #    0.011 K/sec\n                 1      cpu-migrations            #    0.000 K/sec\n                 1      page-faults               #    0.000 K/sec\n    10,176,974,023      cycles                    #    0.312 GHz\n    13,010,322,410      instructions              #    1.28  insn per cycle\n     1,002,214,919      branches                  #   30.699 M/sec\n           123,960      branch-misses             #    0.01% of all branches\n\n       2.043727462 seconds time elapsed\n# perf record -a ./a.out\n&a=0x5589cc1fd038\n&b=0x5589cc1fd034\n[ perf record: Woken up 3 times to write data ]\n[ perf record: Captured and wrote 0.997 MB perf.data (9269 samples) ]\n# perf annotate\n
Run Code Online (Sandbox Code Playgroud)\n

我的观察:

\n
    \n
  • 从1.28insn per cycle我得出结论,程序主要是内存限制的。
  • \n
  • a并且b似乎位于同一高速缓存行中,彼此相邻。
  • \n
\n

我的问题:

\n
    \n
  • 对于各种内存加载和存储,CPU 时间难道不应该更加一致吗?
  • \n
  • 为什么第一次内存加载 ( mov a,%edx) 的 CPU 时间为零?
  • \n
  • 为什么第三次加载的时间是mov a,%ecx0.04%,而紧接着的一次加载的时间是mov b,%edx22.39%?
  • \n
  • 为什么有些指令需要 0 时间?该循环由 14 条指令组成,因此每条指令必须贡献一些可观察的时间。
  • \n
\n

笔记:

\n

操作系统:Linux 4.19.0-amd64,CPU:Intel Core i9-9900K,100%空闲系统(也在i7-7700上测试,结果相同)。

\n

Pet*_*des 5

不完全是“内存”限制,而是存储转发延迟的限制。i9-9900K 和 i7-7700 每个核心的微架构完全相同,因此这并不奇怪:P https://en.wikichip.org/wiki/intel/microarchitectures/coffee_lake#Key_changes_from_Kaby_Lake。(除了可能改进硬件缓解 Meltdown 的方法,以及可能修复循环缓冲区 (LSD) 之外。)

请记住,当性能事件计数器溢出并触发样本时,无序超标量 CPU 必须准确选择一条正在运行的指令来“指责”此cycles事件。通常,这是 ROB 中最旧的未退役指令,或之后的指令。cycles对非常小规模的事件样本 保持高度怀疑。

Perf 永远不会责怪产生结果缓慢的负载,通常是正在等待它的指令。(在本例中为xoradd)。在这里,有时存储会消耗该异或的结果。这些不是缓存未命中加载;而是缓存未命中加载。Skylake 上的存储转发延迟只有大约 3 到 5 个周期(可变的,如果您不尽快尝试,则时间会更短:使用函数调用的循环比空循环更快),因此您确实有大约每 3 到 5 个周期 2 个周期完成负载循环。

通过内存你有两个依赖链

  • 最长的一个涉及两个 RMW b。这是两倍长,并且将成为循环的整体瓶颈。
  • 另一个涉及一个RMW a(每次迭代都有一次额外的读取,这可以与下一次迭代的一部分的读取并行发生a ^= i;)。

for 的 dep 链i只涉及寄存器,并且可以远远领先于其他链;毫无意义,这并不奇怪add $0x1,%rax。它的执行成本完全隐藏在等待加载的阴影中。

我有点惊讶 . 的计数很重要mov %edx,a。也许有时必须等待涉及b在 CPU 的单个存储数据端口上运行的旧存储微指令。(uops 按照最旧就绪优先的方式分派到端口。x86 uops 到底是如何调度的?

在之前的所有 uops 执行完毕之前,uop 无法退出,因此它可能只是从循环底部的存储中获得一些偏差。Uops 以 4 组为一组退出,因此如果mov %edx,b确实退出,则已执行的 cmp/jcc、 的 mov 负载a以及xor %eax,%edx可以随之退出。这些不是等待的部署链的一部分,因此每当商店准备退出b时,他们总是坐在 ROB 中等待退出。b这是关于如何mov %edx,a获得计数的猜测,尽管不是真正瓶颈的一部分。

存储地址微指令应该全部运行在循环之前,因为它们不必等待先前的迭代:RIP 相对寻址1立即准备就绪。它们可以在端口 7 上运行,或者与端口 2 或 3 的负载竞争。对于负载也是如此:它们可以立即执行并检测它们正在等待的存储,负载缓冲区会监视它并准备好报告何时在存储数据微指令最终运行后,数据准备就绪。

据推测,前端最终将在分配加载缓冲区条目时遇到瓶颈,这将限制后端中可以有多少微指令,而不是 ROB 或 RS 大小。

脚注 1:您带注释的输出仅显示anot a(%rip),所以这很奇怪;如果您以某种方式让它使用 32 位绝对值,或者只是反汇编怪癖而未能显示 RIP 相对值,这并不重要。