如何获得涉及 C++ 标准库的帧指针性能调用堆栈/火焰图?
Posted
技术标签:
【中文标题】如何获得涉及 C++ 标准库的帧指针性能调用堆栈/火焰图?【英文标题】:How can you get frame-pointer perf call stacks/flamegraphs involving the C++ standard library? 【发布时间】:2021-07-05 16:57:47 【问题描述】:我喜欢使用 perf record
收集调用堆栈的 fp
方法,因为它比 dwarf
轻量级且简单。但是,当我查看程序使用 C++ 标准库时得到的调用堆栈/火焰图时,它们是不正确的。
这是一个测试程序:
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int __attribute__((noinline)) stupid_factorial(int x)
std::vector<std::string> xs;
// Need to convert numbers to strings or it will all get inlined
for (int i = 0; i < x; ++i)
std::stringstream ss;
ss << std::setw(4) << std::setfill('0') << i;
xs.push_back(ss.str());
int res = 1;
while(std::next_permutation(xs.begin(), xs.end()))
res += 1;
;
return res;
int main()
std::cout << stupid_factorial(11) << "\n";
这是火焰图:
它是在 Docker 容器中的 Ubuntu 20.04 上通过以下步骤生成的:
g++ -Wall -O3 -g -fno-omit-frame-pointer program.cpp -o 6_stl.bin
# Make sure you have libc6-prof and libstdc++6-9-dbg installed
env LD_LIBRARY_PATH=/lib/libc6-prof/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu/debug:$LD_LIBRARY_PATH perf record -F 1000 --call-graph fp -- ./6_stl.bin
# Make sure you have https://github.com/jonhoo/inferno installed
perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg
这样做的主要问题是并非所有函数都是stupid_factorial
的子函数,例如__memcmp_avx2_movbe
。使用dwarf
,他们是。在更复杂的程序中,我什至在main
之外看到过类似的函数。例如,__dynamic_cast
经常没有父级。
在gdb
中,我总是看到正确的回溯,包括此处未正确显示的函数。是否可以在不自己编译的情况下使用libstdc++
获得正确的fp
调用堆栈(这似乎需要大量工作)?
还有其他一些奇怪的东西,虽然我无法在 Ubuntu 18.04(Docker 容器之外)中重现它们:
libstdc++.so.6.28
中有一个未解析的函数。
在我自己的二进制文件6_stl.bin
的最左边有一个未解析的函数。 dwarf
也是如此。
【问题讨论】:
您可能需要使用-fno-omit-frame-pointer
编译的libstdc++
构建,尽管我预计会直接调用__memcmp_avx2_movbe
(至少在第一次调用完成惰性动态链接之后) ,如果你没有使用-fno-plt
)。 GDB 使用 dwarf (.eh_frame
) 回溯,而不是帧指针,除非文件缺少堆栈展开 dwarf 元数据
你试过用-O0
编译看看有什么不同吗?
@PeterCordes 我担心我需要做这样的事情。 __memcmp_avx2_movbe
在stupid_factorial
之外被调用对我来说是不期望的,因为如果它来自std::cout
不会花费太多时间,并且stupid_factorial
没有内联。
@prehistoricpenguin 没有-O3
,火焰图看起来并不歪斜,但不幸的是,我只对优化代码的性能感兴趣。
@PeterCordes 我尝试使用-fno-omit-frame-pointer
编译libstdc++,虽然它有助于解决一些问题(例如未解决的[6_stl.bin]
符号),但它并没有解决我描述的主要问题。
【参考方案1】:
使用您的代码,20.04 x86_64 ubuntu,perf record --call-graph fp
有和没有-e cycles:u
我有与https://speedscope.app 类似的火焰图(使用perf script > out.txt
准备数据并在webapp 中选择out.txt)。
是否可以在不自己编译的情况下使用 libstdc++ 获得正确的 fp 调用堆栈(这似乎需要大量工作)?
不,调用图方法 'fp' 在 linux 内核代码中以非常简单的方式实现:https://elixir.bootlin.com/linux/v5.4/C/ident/perf_callchain_user - https://elixir.bootlin.com/linux/v5.4/source/arch/x86/events/core.c#L2464
perf_callchain_user(struct perf_callchain_entry_ctx *entry, struct pt_regs *regs)
...
fp = (unsigned long __user *)regs->bp;
perf_callchain_store(entry, regs->ip);
...
// where max_stack is probably around 127 = PERF_MAX_STACK_DEPTH https://elixir.bootlin.com/linux/v5.4/source/include/uapi/linux/perf_event.h#L1021
while (entry->nr < entry->max_stack)
...
if (!valid_user_frame(fp, sizeof(frame)))
break;
bytes = __copy_from_user_nmi(&frame.next_frame, fp, sizeof(*fp));
bytes = __copy_from_user_nmi(&frame.return_address, fp + 1, sizeof(*fp));
perf_callchain_store(entry, frame.return_address);
fp = (void __user *)frame.next_frame;
它无法为 -fomit-frame-pointer 编译代码找到正确的帧。
对于 main -> __memcmp_avx2_movbe 不正确的调用堆栈,perf.data 文件中只有内核生成的调用堆栈数据,没有用户堆栈片段的副本,没有寄存器数据:
setarch x86_64 -R env LD_LIBRARY_PATH=/lib/libc6-prof/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu/debug:$LD_LIBRARY_PATH perf record -F 1000 --call-graph fp -- ./6_stl.bin
perf script -D | less
869122666352078 0xae0 [0x58]: PERF_RECORD_SAMPLE(IP, 0x4002): 12267/12267: 0x7ffff7d51670 period: 2332683 addr: 0
... FP chain: nr:5
..... 0: fffffffffffffe00
..... 1: 00007ffff7d51670
..... 2: 0000555555556452
..... 3: 00007ffff7be90fb
..... 4: 00005555555564de
... thread: 6_stl.bin:12267
...... dso: /usr/lib/libc6-prof/x86_64-linux-gnu/libc-2.31.so
6_stl.bin 12267 869122.666352: 2332683 cycles:
7ffff7d51670 __memcmp_avx2_movbe+0x140 (/usr/lib/libc6-prof/x86_64-linux-gnu/libc-2.31.so)
555555556452 main+0x12 (/home/user/so/68259699/6_stl.bin)
7ffff7be90fb __libc_start_main+0x10b (/usr/lib/libc6-prof/x86_64-linux-gnu/libc-2.31.so)
5555555564de _start+0x2e (/home/user/so/68259699/6_stl.bin)
因此,使用这种方法,用户空间性能工具不能使用任何附加信息来修复调用堆栈。使用 dwarf 方法,每个示例事件都有寄存器和用户堆栈数据的部分转储。
Gdb 可以完全访问实时进程,并且可以使用任何信息、所有寄存器、读取任意数量的用户进程堆栈、读取程序和库的附加调试信息。在 gdb 中进行高级和慢速回溯不受时间或安全性或不间断上下文的限制。 Linux内核应该在短时间内记录性能样本,它不能访问交换的数据或调试部分或调试信息文件,它不应该进行复杂的解析(可能有一些错误)。
libstdc++ 的调试版本可能会有所帮助 (sudo apt install libstdc++6-9-dbg
),但速度很慢。它并没有帮助我找到这个 asm 实现的 __memcmp_avx2_movbe (libc: sysdeps/x86_64/multiarch/memcmp-avx2-movbe.S
) 的丢失回溯
如果您想要完整的回溯,我认为您应该找到如何重新编译一个世界(或仅重新编译目标应用程序使用的所有库)。可能不是使用 Ubuntu 而是使用 gentoo、arch 或 apline 之类的东西会更容易?
如果您只对性能感兴趣,为什么要使用火焰图?平面轮廓将捕获大多数性能数据;非理想火焰图也很有用。
【讨论】:
【参考方案2】:当您查看source code for the __memcmp_avx2_movbe
function 时,您会发现它没有function prologue。
因此,我们应该期望在回溯中跳过__memcmp_avx2_movbe
的直接父帧。最内层的帧仍然会被指令指针正确识别为__memcmp_avx2_movbe
,但由帧指针识别的堆栈上的返回地址将属于祖父。
当 stupid_factorial
函数是 __memcmp_avx2_movbe
的父函数时(因为这两者之间的所有中间函数都是内联的),这可以解释问题的主要问题。其他问题通过使用libstdc++
来解决,如here 所述,使用帧指针编译。
【讨论】:
以上是关于如何获得涉及 C++ 标准库的帧指针性能调用堆栈/火焰图?的主要内容,如果未能解决你的问题,请参考以下文章