perf 有一个子命令 annotate 很好用,如果之前没有用过的话可以看一下用它 debug cpython [1] 和找数据结构的热点字段 [2]。但可能有人不知道,perf annotate 的结果可能是“错误的”。
考虑下面的代码:
#include <stdint.h>
#include <stdio.h>
__attribute__((noinline))
uint64_t hot_div(uint64_t iters) {
uint64_t a = 0x123456789abcdefULL;
uint64_t b = 7;
uint64_t acc = 0;
for (uint64_t i = 0; i < iters; i++) {
asm volatile(
"xor %%rdx, %%rdx\n\t"
"idivq %[b]\n\t"
"add %%rax, %[acc]\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t"
: "+a"(a), [acc]"+r"(acc)
: [b]"r"(b)
: "rdx", "cc");
a += 0x9e3779b97f4a7c15ULL;
}
return acc;
}
int main(void) {
volatile uint64_t x = hot_div(400000000ULL);
printf("%llu\n", (unsigned long long)x);
return 0;
}
idivq 指令应该是最慢的热点,我们预期 perf record + perf annotate 的输出应该指向这个这个指令,在 x64 linux6.17 上运行命令试试:
gcc main.c
perf record -e cycles:u -- taskset -c 2 ./a.out
perf annotate --stdio -l -s hot_div
输出却是 62% 的 add 和 0% 的 idiv?
16.63 : 401165: xor %rdx,%rdx // a.out[401165]
0.00 : 401168: idiv %rsi
62.06 : 40116b: add %rax,%rcx // a.out[40116b]
我是这样肤浅地理解这个问题的,cpu 执行指令需要 decode 同时推进 rip 寄存器,所以如果 cpu 在执行一句指令,此时 rip 其实指向下一条指令;此事在 call 指令上也很明显,压到栈上的 ra (IR) 其实是指向 call 的下一条指令,本质也是因为执行 call 指令的时候 rip 已经指向下一条。
所以对于上面的 perf annotate 输出,真正的热点其实是 62% 的上一条,idiv 指令。
然而真实世界比这更加复杂。我们在相同的 x64 linux6.17 再试一次:
# perf record -- taskset -c 2 ./a.out
# perf annotate --stdio -l -s hot_div
4.20 : 401162: mov %rdx,%rcx // a.out[401162]
0.00 : 401165: xor %rdx,%rdx
70.35 : 401168: idiv %rsi // a.out[401168]
0.00 : 40116b: add %rax,%rcx
这次 perf annotate 又准确地指向了 idiv 热点 (70%),为什么?
简单来说 x64 的 perf 在内核里实现了一个叫做 precise_ip 的东西,上面两次 perf record 的微小参数差异导致调用的 perf_event_open syscall 的参数有差异,第一个命令的 precise_ip=3,第二个的 precise_ip=0。如果 precise_ip=3,在内核里会对 rip 做一次修正,这在 intel 里叫做 PEBS (Precise Event-Based Sampling)。这项功能的 fixup 做得非常精细,考虑到上一个指令其实可能是 goto 跳过来到当前 rip 的, intel cpu 通过查询 LBR 准确地回溯到上一次跳转来修正 rip,非常炫。
不过实际使用的时候并不需要观测 syscall 参数,而是通过 perf evlist 直接看采样文件就能知道是否启动了 precise_ip 修正:
perf evlist -v -i perf.data
如果输出里有 "precise_ip: 3",说明采样结果已修正;如果没有 precise_ip,说明有 off-by-one。
以上只针对 x64 的情况,如果是 arm64 又会怎样呢?
不知,没有 arm64 的机器,但 gpt-5.3-codex-medium 说 arm64 有个 SPE 的功能可以在 report/annotate 的时候做矫正。听不懂😋
总之用 perf 要小心了,如果不理解这些实现细节小心得出完全错误的 profiling 结论😉
[1] debug cpython https://t.me/c/1459082815/900
[2] 找数据结构的热点字段 https://t.me/c/1459082815/1015
考虑下面的代码:
#include <stdint.h>
#include <stdio.h>
__attribute__((noinline))
uint64_t hot_div(uint64_t iters) {
uint64_t a = 0x123456789abcdefULL;
uint64_t b = 7;
uint64_t acc = 0;
for (uint64_t i = 0; i < iters; i++) {
asm volatile(
"xor %%rdx, %%rdx\n\t"
"idivq %[b]\n\t"
"add %%rax, %[acc]\n\t"
"nop\n\t"
"nop\n\t"
"nop\n\t"
: "+a"(a), [acc]"+r"(acc)
: [b]"r"(b)
: "rdx", "cc");
a += 0x9e3779b97f4a7c15ULL;
}
return acc;
}
int main(void) {
volatile uint64_t x = hot_div(400000000ULL);
printf("%llu\n", (unsigned long long)x);
return 0;
}
idivq 指令应该是最慢的热点,我们预期 perf record + perf annotate 的输出应该指向这个这个指令,在 x64 linux6.17 上运行命令试试:
gcc main.c
perf record -e cycles:u -- taskset -c 2 ./a.out
perf annotate --stdio -l -s hot_div
输出却是 62% 的 add 和 0% 的 idiv?
16.63 : 401165: xor %rdx,%rdx // a.out[401165]
0.00 : 401168: idiv %rsi
62.06 : 40116b: add %rax,%rcx // a.out[40116b]
我是这样肤浅地理解这个问题的,cpu 执行指令需要 decode 同时推进 rip 寄存器,所以如果 cpu 在执行一句指令,此时 rip 其实指向下一条指令;此事在 call 指令上也很明显,压到栈上的 ra (IR) 其实是指向 call 的下一条指令,本质也是因为执行 call 指令的时候 rip 已经指向下一条。
所以对于上面的 perf annotate 输出,真正的热点其实是 62% 的上一条,idiv 指令。
然而真实世界比这更加复杂。我们在相同的 x64 linux6.17 再试一次:
# perf record -- taskset -c 2 ./a.out
# perf annotate --stdio -l -s hot_div
4.20 : 401162: mov %rdx,%rcx // a.out[401162]
0.00 : 401165: xor %rdx,%rdx
70.35 : 401168: idiv %rsi // a.out[401168]
0.00 : 40116b: add %rax,%rcx
这次 perf annotate 又准确地指向了 idiv 热点 (70%),为什么?
简单来说 x64 的 perf 在内核里实现了一个叫做 precise_ip 的东西,上面两次 perf record 的微小参数差异导致调用的 perf_event_open syscall 的参数有差异,第一个命令的 precise_ip=3,第二个的 precise_ip=0。如果 precise_ip=3,在内核里会对 rip 做一次修正,这在 intel 里叫做 PEBS (Precise Event-Based Sampling)。这项功能的 fixup 做得非常精细,考虑到上一个指令其实可能是 goto 跳过来到当前 rip 的, intel cpu 通过查询 LBR 准确地回溯到上一次跳转来修正 rip,非常炫。
不过实际使用的时候并不需要观测 syscall 参数,而是通过 perf evlist 直接看采样文件就能知道是否启动了 precise_ip 修正:
perf evlist -v -i perf.data
如果输出里有 "precise_ip: 3",说明采样结果已修正;如果没有 precise_ip,说明有 off-by-one。
以上只针对 x64 的情况,如果是 arm64 又会怎样呢?
不知,没有 arm64 的机器,但 gpt-5.3-codex-medium 说 arm64 有个 SPE 的功能可以在 report/annotate 的时候做矫正。听不懂😋
总之用 perf 要小心了,如果不理解这些实现细节小心得出完全错误的 profiling 结论😉
[1] debug cpython https://t.me/c/1459082815/900
[2] 找数据结构的热点字段 https://t.me/c/1459082815/1015