如何获得涉及 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_movbedwarf,他们是。在更复杂的程序中,我什至在 main 之外看到过这样的函数。 __dynamic_cast 例如,通常没有 parent。

gdb 中,我总能看到正确的回溯,包括此处未正确显示的函数。是否可以在不自己编译的情况下使用 libstdc++ 获得正确的 fp 调用堆栈(这似乎需要很多工作)?

还有其他奇怪的地方,虽然我无法在 Ubuntu 18.04 中重现它们(在 Docker 容器之外):

使用你的代码,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.

所述