如何获得涉及 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_movbestupid_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 &gt; 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++ 标准库的帧指针性能调用堆栈/火焰图?的主要内容,如果未能解决你的问题,请参考以下文章

如何在堆栈上分配数组以获得性能提升?

C#调用C++类库的几种方式

迭代 unordered_map 时如何获取指向键的指针?

调用堆栈上大量对象的构造函数

系统调用和函数调用的区别

在 C++ 中将调用堆栈扩展到磁盘?