如何获得涉及 C++ 标准库的 frame-pointer perf call stacks/flamegraphs?
How can you get frame-pointer perf call stacks/flamegraphs involving the C++ standard library?
我喜欢使用 perf record
收集调用堆栈的 fp
方法,因为它比 dwarf
更轻巧且更简单。但是,当我查看程序使用 C++ 标准库时得到的调用 stacks/flamegraphs,它们是不正确的。
这是一个测试程序:
#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";
}
这是火焰图:
它是在 Ubuntu 20.04 的 Docker 容器中通过以下步骤生成的:
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
的 children,例如__memcmp_avx2_movbe
。 dwarf
,他们是。在更复杂的程序中,我什至在 main
之外看到过这样的函数。 __dynamic_cast
例如,通常没有 parent。
在 gdb
中,我总能看到正确的回溯,包括此处未正确显示的函数。是否可以在不自己编译的情况下使用 libstdc++
获得正确的 fp
调用堆栈(这似乎需要很多工作)?
还有其他奇怪的地方,虽然我无法在 Ubuntu 18.04 中重现它们(在 Docker 容器之外):
libstdc++.so.6.28
中有一个未解析的函数。
- 在我自己的二进制文件
6_stl.bin
的最左边有一个未解析的函数。 dwarf
. 也是如此
使用你的代码,20.04 x86_64 ubuntu,perf record --call-graph fp
有和没有 -e cycles:u
我有与 https://speedscope.app 相似的火焰图(准备数据perf script > out.txt
和 select out.txt 在网络应用程序中)。
Is it possible to get correct fp call stacks with libstdc++ without compiling it myself (which seems like a lot of work)?
不,调用图方法 '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)
因此,使用此方法 user-space perf 工具无法使用任何其他信息来修复调用堆栈。使用 dwarf 方法,每个样本事件都有寄存器和用户堆栈数据的部分转储。
Gdb 可以完全访问实时进程,可以使用任何信息、所有寄存器、读取任意数量的用户进程堆栈、读取程序和库的附加调试信息。并且在 gdb 中进行高级和慢速回溯不受时间或安全性或不间断上下文的限制。 Linux 内核应该在短时间内记录 perf 样本,它不能访问交换数据或调试部分或调试信息文件,它不应该进行复杂的解析(可能会有一些错误)。
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 之类的东西会更容易?
如果您只对性能感兴趣,为什么要火焰图?平面配置文件将捕获大部分性能数据;非理想的火焰图也很有用。
当你看 source code for the __memcmp_avx2_movbe
function, you see that it doesn't have a function prologue.
因此,我们应该期望 __memcmp_avx2_movbe
的直接 parent 帧在回溯中被跳过。最内层的帧仍然会被指令指针正确识别为 __memcmp_avx2_movbe
,但是帧指针所识别的堆栈上的 return 地址将属于 grandparent.
当 stupid_factorial
函数是 __memcmp_avx2_movbe
的 parent 时(因为这两者之间的所有中间函数都是内联的),这可以解释问题的主要问题。其他问题通过使用 libstdc++
编译的帧指针解决,如 here.
所述
我喜欢使用 perf record
收集调用堆栈的 fp
方法,因为它比 dwarf
更轻巧且更简单。但是,当我查看程序使用 C++ 标准库时得到的调用 stacks/flamegraphs,它们是不正确的。
这是一个测试程序:
#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";
}
这是火焰图:
它是在 Ubuntu 20.04 的 Docker 容器中通过以下步骤生成的:
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
的 children,例如__memcmp_avx2_movbe
。 dwarf
,他们是。在更复杂的程序中,我什至在 main
之外看到过这样的函数。 __dynamic_cast
例如,通常没有 parent。
在 gdb
中,我总能看到正确的回溯,包括此处未正确显示的函数。是否可以在不自己编译的情况下使用 libstdc++
获得正确的 fp
调用堆栈(这似乎需要很多工作)?
还有其他奇怪的地方,虽然我无法在 Ubuntu 18.04 中重现它们(在 Docker 容器之外):
libstdc++.so.6.28
中有一个未解析的函数。- 在我自己的二进制文件
6_stl.bin
的最左边有一个未解析的函数。dwarf
. 也是如此
使用你的代码,20.04 x86_64 ubuntu,perf record --call-graph fp
有和没有 -e cycles:u
我有与 https://speedscope.app 相似的火焰图(准备数据perf script > out.txt
和 select out.txt 在网络应用程序中)。
Is it possible to get correct fp call stacks with libstdc++ without compiling it myself (which seems like a lot of work)?
不,调用图方法 '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)
因此,使用此方法 user-space perf 工具无法使用任何其他信息来修复调用堆栈。使用 dwarf 方法,每个样本事件都有寄存器和用户堆栈数据的部分转储。
Gdb 可以完全访问实时进程,可以使用任何信息、所有寄存器、读取任意数量的用户进程堆栈、读取程序和库的附加调试信息。并且在 gdb 中进行高级和慢速回溯不受时间或安全性或不间断上下文的限制。 Linux 内核应该在短时间内记录 perf 样本,它不能访问交换数据或调试部分或调试信息文件,它不应该进行复杂的解析(可能会有一些错误)。
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 之类的东西会更容易?
如果您只对性能感兴趣,为什么要火焰图?平面配置文件将捕获大部分性能数据;非理想的火焰图也很有用。
当你看 source code for the __memcmp_avx2_movbe
function, you see that it doesn't have a function prologue.
因此,我们应该期望 __memcmp_avx2_movbe
的直接 parent 帧在回溯中被跳过。最内层的帧仍然会被指令指针正确识别为 __memcmp_avx2_movbe
,但是帧指针所识别的堆栈上的 return 地址将属于 grandparent.
当 stupid_factorial
函数是 __memcmp_avx2_movbe
的 parent 时(因为这两者之间的所有中间函数都是内联的),这可以解释问题的主要问题。其他问题通过使用 libstdc++
编译的帧指针解决,如 here.