【问题标题】:Capturing function exit time with __gnu_mcount_nc使用 __gnu_mcount_nc 捕获函数退出时间
【发布时间】:2014-11-09 23:54:14
【问题描述】:

我正在尝试在支持不佳的原型嵌入式平台上进行一些性能分析。

我注意到 GCC 的 -pg 标志会导致在每个函数的入口处插入 __gnu_mcount_nc 的 thunk。没有__gnu_mcount_nc 的实现可用(并且供应商对提供帮助不感兴趣),但是由于编写一个简单地记录堆栈帧和当前循环计数的程序是微不足道的,我已经这样做了;这工作得很好,并且在调用者/被调用者图和最常调用的函数方面产生了有用的结果。

我真的很想获得有关在函数体中花费的时间的信息,但是我很难理解如何只通过入口而不是出口来处理这个问题,每个函数都被钩住了:你可以确切地知道当每个函数被输入,但没有钩住退出点时,你无法知道有多少时间,直到你收到下一条信息来归因于被调用者以及多少给调用者。

尽管如此,GNU 分析工具实际上能够为许多平台上的函数收集运行时信息,因此开发人员大概有一些计划来实现这一点。

我已经看到了一些现有的实现,例如维护影子调用堆栈并在进入 __gnu_mcount_nc 时调整返回地址,以便在被调用者返回时再次调用 __gnu_mcount_nc;然后它可以将调用者/被调用者/sp 三元组与影子调用堆栈的顶部进行匹配,从而将这种情况与入口调用区分开来,记录退出时间并正确返回给调用者。

这种方法还有很多不足之处:

  • 在存在递归和没有 -pg 标志编译的库的情况下,它似乎很脆弱
  • 在缺乏工具链 TLS 支持且当前线程 ID 可能很昂贵/获取复杂性的嵌入式多线程/多核环境中,似乎很难以低开销实现或根本难以实现

是否有一些明显更好的方法来实现 __gnu_mcount_nc 以便 -pg 构建能够捕获我缺少的函数退出和进入时间?

【问题讨论】:

    标签: c++ gcc profiling gprof


    【解决方案1】:

    gprof 不将该函数用于计时、进入退出,而是用于函数 A 调用任何函数 B 的调用计数。 相反,它使用通过计算每个例程中的 PC 样本收集的自时间,然后使用函数到函数的调用计数来估计应该将多少自时间计回给调用者。

    例如,如果 A 调用 C 10 次,B 调用 C 20 次,并且 C 有 1000ms 的自身时间(即 100 个 PC 样本),那么 gprof 知道 C 已被调用 30 次,其中 33 个样品可以计入 A,而其他 67 个样品可以计入 B。 同样,样本计数会沿调用层次结构向上传播。

    所以你看,它没有时间函数进入和退出。 它得到的测量结果非常粗略,因为它没有区分短调用和长调用。 此外,如果 PC 样本发生在 I/O 期间或未使用 -pg 编译的库例程中,则根本不计算在内。 而且,正如您所指出的,它在存在递归的情况下非常脆弱,并且会在短函数上引入显着的开销。

    另一种方法是堆栈采样,而不是 PC 采样。 诚然,捕获堆栈样本比 PC 样本更昂贵,但需要的样本更少。 例如,如果一个函数、代码行或您想要进行的任何描述在 N 个样本总数中的分数 F 上很明显,那么您知道它花费的时间分数是 F,具有标准偏差sqrt(NF(1-F))。 因此,例如,如果您抽取 100 个样本,其中 50 个样本上出现了一行代码,那么您可以估计该行花费 50% 的时间,不确定性为 sqrt(100*.5*.5) = +/- 5 个样本或介于 45% 和 55% 之间。 如果您抽取 100 倍的样本,您可以将不确定性降低 10 倍。 (递归无关紧要。如果一个函数或代码行在单个样本中出现 3 次,则算作 1 个样本,而不是 3 个。 函数调用是否很短也没关系——如果它们被调用的次数足够多,花费了很大一部分,它们就会被捕获。)

    请注意,当您正在寻找可以通过修复来获得加速的问题时,确切的百分比并不重要。 重要的是找到它。 (事实上​​,你只需要看到一个问题两次就知道它大到可以解决。)

    那是this technique


    附:不要被调用图、热点路径或热点所吸引。 这是一个典型的调用图鼠巢。黄色是热点路径,红色是热点。

    这表明在这些地方都没有一个多汁的加速机会是多么容易:

    最有价值的是十几个随机原始堆栈样本,并将它们与源代码相关联。 (这意味着绕过分析器的后端。)

    添加:为了说明我的意思,我从上面的调用图中模拟了十个堆栈样本,这就是我发现的

    • 3/10 个样本调用class_exists,一个用于获取类名,两个用于设置本地配置。 class_exists 调用 autoload,后者调用 requireFile,其中两个调用 adminpanel。如果可以更直接地做到这一点,可以节省大约 30%。
    • 2/10 个样本正在调用determineId,它调用fetch_the_id,它调用getPageAndRootlineWithDomain,它又调用三个级别,终止于sql_fetch_assoc。获取 ID 似乎很麻烦,而且要花费大约 20% 的时间,这还不包括 I/O。

    因此,堆栈示例不仅告诉您一个函数或一行代码花费了多少包含时间,它们还告诉您为什么要完成它,以及完成它需要多少愚蠢。 我经常看到这种情况——疾驰的普遍性——用锤子拍苍蝇,不是故意的,而是遵循良好的模块化设计。

    添加:另一件不要被卷入的事情是火焰图。 例如,这是来自上述调用图中的 10 个模拟堆栈样本的火焰图(向右旋转 90 度)。例程都是编号的,而不是命名的,但每个例程都有自己的颜色。
    注意我们在上面发现的问题,class_exists(例程 219)在 30% 的样本上,通过查看火焰图一点也不明显。 更多的样本和不同的颜色会使图形看起来更像“火焰”,但不会暴露从不同地方多次调用而需要大量时间的例程。

    这是按功能而不是按时间排序的相同数据。 这有点帮助,但不会汇总从不同地方调用的相似性:
    再一次,目标是找到对你隐藏的问题。 任何人都可以找到简单的东西,但隐藏的问题才是最重要的。

    添加:另一种眼睛糖果是这个:
    黑色轮廓的例程可能都是相同的,只是从不同的地方调用。 该图不会为您汇总它们。 如果一个套路在不同地方被大量调用,具有很高的包容率,它就不会被暴露。

    【讨论】:

      猜你喜欢
      • 2018-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-01
      • 2011-12-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多