gpt4 book ai didi

performance - `perf annotate` 内存加载/存储时间报告不一致

转载 作者:行者123 更新时间:2023-12-03 08:28:32 26 4
gpt4 key购买 nike

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

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

#include <stdint.h>
#include <stdio.h>

volatile uint32_t a;
volatile uint32_t b;

int main() {
printf("&a=%p\n&b=%p\n", &a, &b);
for(size_t i = 0; i < 1000000000LL; i++) {
a ^= (uint32_t) i;
b += (uint32_t) i;
b ^= a;
}
return 0;
}

我用gcc -O2编译它并在perf下运行:

# gcc -g -O2 a.c
# perf stat -a ./a.out
&a=0x55a4bcf5f038
&b=0x55a4bcf5f034

Performance counter stats for 'system wide':

32,646.97 msec cpu-clock # 15.974 CPUs utilized
374 context-switches # 0.011 K/sec
1 cpu-migrations # 0.000 K/sec
1 page-faults # 0.000 K/sec
10,176,974,023 cycles # 0.312 GHz
13,010,322,410 instructions # 1.28 insn per cycle
1,002,214,919 branches # 30.699 M/sec
123,960 branch-misses # 0.01% of all branches

2.043727462 seconds time elapsed
# perf record -a ./a.out
&a=0x5589cc1fd038
&b=0x5589cc1fd034
[ perf record: Woken up 3 times to write data ]
[ perf record: Captured and wrote 0.997 MB perf.data (9269 samples) ]
# perf annotate

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

Percent│      for(size_t i = 0; i < 1000000000LL; i ++) {
│ xor %eax,%eax
│ nop
│ a ^= (uint32_t) i;
│28: mov a,%edx // 32-bit load
│ xor %eax,%edx
9.74 │ mov %edx,a // 32-bit store
│ b += (uint32_t) i;
12.12 │ mov b,%edx // 32-bit load
8.79 │ add %eax,%edx
│ for(size_t i = 0; i < 1000000000LL; i ++) {
│ add $0x1,%rax
│ b += (uint32_t) i;
18.69 │ mov %edx,b // 32-bit store
│ b ^= a;
0.04 │ mov a,%ecx // 32-bit load
22.39 │ mov b,%edx // 32-bit load
8.92 │ xor %ecx,%edx
19.31 │ mov %edx,b // 32-bit store
│ for(size_t i = 0; i < 1000000000LL; i ++) {
│ cmp $0x3b9aca00,%rax
│ ↑ jne 28
│ }
│ return 0;
│ }
│ xor %eax,%eax
│ add $0x8,%rsp
│ ← retq

我的观察:

  • 从 1.28 insn 每周期我得出结论,该程序主要受内存限制。
  • ab 似乎位于同一缓存行中,彼此相邻。

我的问题:

  • 对于各种内存加载和存储,CPU 时间不应该更加一致吗?
  • 为什么第一次内存加载 (mov a,%edx) 的 CPU 时间为零?
  • 为什么第三次加载 mov a,%ecx 的时间是 0.04%,而紧邻的一个 mov b,%edx 的时间是 22.39%?<
  • 为什么有些指令需要 0 时间?该循环由 14 条指令组成,因此每条指令必须贡献一些可观察的时间。

注释:

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

最佳答案

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

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

Perf 永远不会责怪产生结果缓慢的负载,通常是正在等待它的指令。 (在本例中为xoradd)。在这里,有时存储会消耗该异或的结果。这些不是缓存未命中加载;而是缓存未命中加载。 Skylake 上的存储转发延迟仅为大约 3 到 5 个周期(可变,如果您不尽快尝试的话会更短: Loop with function call faster than an empty loop ),因此您确实每 3 到 5 个周期完成大约 2 个负载。

内存中有两个依赖链

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

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

我有点惊讶 mov %edx,a 的计数很高。也许有时必须等待涉及 b 的旧存储 uops 在 CPU 的单个存储数据端口上运行。 (Uops按照最旧的就绪优先调度到端口。How are x86 uops scheduled, exactly?)

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

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

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

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

关于performance - `perf annotate` 内存加载/存储时间报告不一致,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65906312/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com