3

我喜欢fp收集调用堆栈的方法,perf record因为它比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中,我总是看到正确的回溯,包括此处未正确显示的函数。是否有可能在不自己编译的情况下获得正确的fp调用堆栈libstdc++(这似乎需要做很多工作)?

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

  • 中有一个未解决的函数libstdc++.so.6.28
  • 我自己的二进制文件中有一个未解析的函数6_stl.bin,位于最左边。情况也是如此dwarf
4

2 回答 2

3

使用您的代码 20.04 x86_64 ubuntu,perf record --call-graph fp无论有无-e cycles:u我都有与https://speedscope.app相似的火焰图(使用 webapp 准备数据perf script > out.txt并选择 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 之类的东西会更容易?

如果您只对性能感兴趣,为什么要使用火焰图?平面轮廓将捕获大多数性能数据;非理想火焰图也很有用。

于 2021-07-06T13:04:15.820 回答
2

当您查看函数的源代码时__memcmp_avx2_movbe,您会发现它没有函数序言

__memcmp_avx2_movbe因此,我们应该期望在回溯中跳过直接父帧。最内层的帧仍将被正确识别为__memcmp_avx2_movbe来自指令指针,但由帧指针识别的堆栈上的返回地址将属于祖父母。

stupid_factorial函数是父函数时__memcmp_avx2_movbe(因为这两者之间的所有中间函数都是内联的),这可以解释问题的主要问题。其他问题可以通过使用libstdc++带有帧指针的编译器来解决,如此所述。

于 2021-07-16T16:46:46.020 回答