Linux perf record -e cycles -g 的调用栈采样与 –call-graph dwarf 精度对比

2次阅读

perf record -g 默认用的是 frame pointer,等价于 –call-graph fp,依赖指针链,开销小但遇内联、尾调用或 jit 代码易截断;dwarf 模式需显式指定且依赖调试信息、内核支持及正确参数。

Linux perf record -e cycles -g 的调用栈采样与 –call-graph dwarf 精度对比

perf record -g 默认用的是 frame pointer 还是 DWARF?

perf record -g 在大多数现代 linux 发行版(如 ubuntu 22.04+、RHEL 8+)上默认启用 frame pointer 模式,不是 DWARF。它等价于 --call-graph fp,前提是内核编译时开了 CONFIG_FRAME_POINTER=y,且用户程序没被编译成 -fomit-frame-pointer(GCC/Clang 默认已禁用该优化)。

  • 默认 fp 模式依赖帧指针链,快、开销小,但遇到内联函数、尾调用、手写汇编或某些 JIT 代码时会截断
  • --call-graph dwarf 读取 ELF 中的 .debug_frame.eh_frame,能还原更完整的调用栈,尤其对优化过的代码更可靠
  • 但 DWARF 采样开销明显更高:每次样本都要解析调试信息 + 栈回溯,CPU 占用高、可能丢样本、perf script 解析也慢得多

什么时候必须用 –call-graph dwarf?

当你观察到以下现象时,fp 模式大概率不够用:

  • perf report 里大量调用栈只显示 1–2 层,尤其是从 libclibstdc++ 进入后就断了
  • 程序用 -O2 -fno-omit-frame-pointer 编译,但仍有函数“消失”在调用路径中(比如 std::vector::push_back 后直接跳到 malloc
  • 调试对象是 Go、rust(未开启 frame-pointers)、Java(jvm 需额外配置)或 Python 扩展模块,它们默认不维护传统帧指针
  • 你看到 perf script 输出里有大量 [unknown]__kernel_rt_sigreturn 卡在栈顶,说明帧链已损坏

这时要强制切到 DWARF:perf record -e cycles --call-graph dwarf -g ./myapp

DWARF 模式下必须确保的三件事

--call-graph dwarf 不是开箱即用,漏掉任一环节都会退化成空栈或报错:

  • 用户二进制必须带调试信息:gcc -g -O2rustc -g;strip 过的文件不行,readelf -S ./a.out | grep debug 应能看到 .debug_*
  • 内核需支持 DWARF 栈展开:检查 cat /proc/sys/kernel/perf_event_paranoid ≤ 2,且内核配置含 CONFIG_UNWINDER_DWARF(主流发行版内核通常已启用)
  • 不要混用 -g--call-graph:写成 perf record -e cycles -g --call-graph dwarf 会静默忽略 --call-graph,正确写法是去掉 -g,只用 --call-graph dwarf

cycles 事件 + DWARF 的性能代价真实有多高?

别只看文档说“开销大”,实测差异很具体:

  • 同样采样 10 秒、cycles 事件、1ms 间隔下:fp 模式 perf.data 约 8–12 MB;dwarf 模式常达 40–90 MB,且 perf script 解析时间从 0.3s 拉长到 5–12s
  • 在高频短函数(如 hash 表查找、锁竞争点)场景,DWARF 可能因处理不过来而丢弃 15–30% 的样本(perf report -D | grep lost 可查)
  • 如果目标是定位热点函数而非完整调用链,优先用 --call-graph lbr(Intel CPU 支持)或干脆不用 -g,靠 perf report --no-children 看 flat profile 更稳

调用栈精度和采样保真度之间始终存在张力,DWARF 不是银弹,它解决的是“能不能看到”,但代价是“看到多少”和“还能不能信”。

text=ZqhQzanResources