回溯查询非常慢

Backtrace Queries Extremely Slow

(注:以下提到execinfo/ backtrace,但这只是一个例子。问题中的行为出现在各种库中。)


考虑一个实用程序库,它跟踪与其链接的某些应用程序的资源分配。当函数分配和释放资源时,它们会调用一个记录操作细节的跟踪函数,以及一些可用于重建调用路径的信息。有时,会通过调用路径查询库中的操作细目。

在本题的设置中,要求tracking开销小,而querying则不一定。因此,为了跟踪,我存储了识别调用路径的最少信息,例如,通过调用 execinfo/ backtrace。翻译符号、拆解等,推迟到查询,不是这个问题的一部分。

令我惊讶的是,相对于 malloc 调用,简单地调用 backtrace 会使执行速度减慢 ~4000%(!)。由于 backtrace 将请求的(最大)堆栈深度作为参数,并且可以通过具有不同堆栈深度的调用路径调用它,所以我试图了解这些参数如何影响其性能。据我所知,以任何方式简单地调用此函数都会带来巨大的损失。

为了测量,我编写了以下简单代码(另请参阅 full version):

const size_t max_backtrace_size = 100;
void *backtrace_buf[max_backtrace_size];   
static void track()
{
    if(backtrace_size > 0)
        ::backtrace(backtrace_buf, backtrace_size);
}

static void op(size_t i)
{
    if(i == 0)
    {
        track();
        return;
    }
    op(i - 1);
}

这两个函数中的第一个,track,模拟实际跟踪(注意 backtrace_size == 0 完全禁用对 backtrace 的调用);第二个 op 是一个通过调用 track 终止的递归。使用这两个函数,我改变了参数并测量了结果(另请参阅 the IPython Notebook)。

下图显示了跟踪时间,作为不同堆栈大小的函数,对于每个调用 backtracebacktrace_size == 1 或不调用它(它的时间很短,它位于X 轴,并且在图中几乎看不到)。 backtrace 即使使用小参数调用也会产生巨大的开销。

下图进一步显示了开销,现在是回溯大小和堆栈深度的函数。同样,只需调用此函数就有一个巨大的跳跃。

  1. 有什么技术方法可以减少寻找回溯的开销吗? (可能是不同的库,或不同的构建设置。)

  2. 在缺少 1. 的情况下,是否有完全不同的可行方案来解决上面的问题?

To my surprise, simply calling backtrace slowed down execution by ~4000%(!).

这种说法本身是没有意义的。即使 backtrace() 归结为 单个 指令,如果您的其他代码根本不包含任何指令,它仍然会构成 +INF 开销。

40 倍的开销可能意味着您要考虑的资源的获取成本非常低。如果是这样,也许不必考虑该资源的 每个 实例?例如,您能否仅记录每个 N 资源分配的堆栈跟踪?

Is there any technical way to reduce the overhead of finding the backtrace? (perhaps a different library, or different build settings.)

有几种可能性需要考虑。

假设您问的是 Linux/x86_64,backtrace 速度慢的一个原因是在没有帧指针的情况下它必须查找和解释展开信息。另一个原因:它主要用于处理异常,并且从未针对速度进行过优化。

如果您可以完全控制将使用您的库的应用程序,使用 -fno-omit-frame-pointer 构建 everything 将允许您使用更快的基于帧的展开器。

如果你做不到这一点,libunwind backtrace 可能比 GLIBC 快得多(尽管它仍然无法击败基于帧的展开器)。

你只是说 backtracemalloc 花费更多的时间。 如果你需要它告诉你的东西,那就要付出代价。

是否曾经打算超级高效,以便可以高频调用它?

我确定它的目的是诊断诸如内存问题之类的问题,您经常调用它,或者性能问题,您不需要经常调用它。

当你发现问题时,你可以解决它们, 当您不再需要 backtrace 时,您可以停止调用它,并很高兴它能帮助您找到它们。

如果您使用的是 libunwind,请确保使用 UNW_LOCAL_ONLY 定义的方式构建代码:

#define UNW_LOCAL_ONLY
#include <unwind/libunwind.h>

我发现它也有助于将“--disable-block-signals”添加到配置命令 - 没有它,libunwind 最终可能会花费大量时间在代码的某些部分周围阻止和取消阻止信号。在 ARM(我正在测试的地方)上,这非常重要。

即便如此,我认为 libunwind 的性能仍有一些改进空间。我正在使用 perf 来尝试对此进行更多挖掘。