【问题标题】:Backtrace Queries Extremely Slow回溯查询极慢
【发布时间】:2015-05-12 21:44:15
【问题描述】:

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


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

在本题的设置中,要求跟踪是低开销的,但查询不一定。因此,为了跟踪,我存储了标识调用路径的最少信息,例如,通过调用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. 的情况下,是否有完全不同的可行替代方案来解决顶部问题?

【问题讨论】:

    标签: c++ linux g++ profiling glibc


    【解决方案1】:

    令我惊讶的是,简单地调用 backtrace 会使执行速度减慢了 ~4000%(!)。

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

    40 倍的开销可能意味着您尝试考虑的资源非常便宜。如果是这样,也许不是必须考虑该资源的每个实例?例如,您能否仅记录每个 Nth 资源分配的堆栈跟踪?

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

    有几种可能性需要考虑。

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

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

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

    【讨论】:

    • 感谢您的回答!你在开始时写的大部分内容。相对开销是正确的,但不幸的是,我无意中忽略了 a malloc 调用 的相对开销(我会在问题中更正这一点 - 谢谢)。关于采样——这就是我最终要做的。你关于堆栈帧指针的观点特别有启发性,我会进一步阅读。
    【解决方案2】:

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

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

    我确信它的目的是诊断诸如内存问题(您经常调用它)或性能问题(您不需要经常调用它)等问题。

    当您发现问题时,您可以修复它们, 当您不再需要 backtrace 时,您可以停止调用它,并为它帮助您找到它们而感到高兴。

    【讨论】:

    • 感谢您的回答。该库检查在野外部署的服务器中的问题,这些服务器必须同时服务请求。不幸的是,它通常用于在实验室中也不容易复制的场景。所以启用-修复-禁用有点复杂,没有解决“修复”阶段的效率问题。
    • @AmiTavory:但是,它是否有效完全取决于它所花费的时间百分比,这取决于每秒需要调用多少次。在 SO 上有 如此 很多问题,人们会说“我怎样才能加快库例程 Foo?”,而无需质疑他们是否真的需要像他们一样经常调用它。
    • 再次感谢。你当然是对的,这实际上是电话的(#/时间)分数的问题。在这种情况下,跟踪与由服务器客户端发起的操作引起的mallocs 相关联,并且客户端请求的速率超出了我们的控制范围。我们一直在通过仅随机抽样一些操作(这基本上是您的想法的实现)来处理这个问题。尽管如此,跟踪效率越高,采样率就越高(噪声越小)。因此是最初的问题。
    【解决方案3】:

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

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

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

    即使在此之后,我认为 libunwind 的性能仍有一些改进空间。我正在使用 perf 来尝试对此进行深入研究。

    【讨论】:

      猜你喜欢
      • 2019-10-25
      • 1970-01-01
      • 1970-01-01
      • 2012-05-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-26
      • 1970-01-01
      相关资源
      最近更新 更多