使用__gnu_mcount_nc捕获函数退出时间

时间:2014-09-16 09:31:41

标签: c++ gcc profiling gprof

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

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

我真的想获得有关在函数体中花费的时间的信息,但是我很难理解如何只使用条目而不是退出来解决这个问题,每个函数都会被钩住:你可以准确地说出来当输入每个函数但没有挂钩退出点时,你不知道有多少时间,直到你收到属于被调用者的下一条信息以及调用者多少。

尽管如此,GNU概要分析工具实际上能够在许多平台上收集函数的运行时信息,因此可能开发人员在实现这一目标时需要考虑一些方案。

我已经看到一些现有的实现,它们执行诸如维护阴影调用堆栈并将入口处的返回地址旋转到__gnu_mcount_nc,以便在被调用者返回时再次调用__gnu_mcount_nc;然后,它可以将调用者/被调用者/ sp三元组与影子调用堆栈的顶部进行匹配,从而将此案例与条目调用区分开来,记录退出时间并正确返回调用者。

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

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

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

1 个答案:

答案 0 :(得分:12)

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

例如,如果A调用C 10次,B调用C 20次,并且C具有1000ms的自身时间(即100个PC样本),则 gprof 知道C已被调用30次,33个样品可以充电到A,而另外67个可以充电到B. 同样,样本计数在调用层次结构中向上传播。

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

另一种方法是堆栈采样,而不是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


P.S。不要被称为呼叫图,热门路径或热点。 这是一个典型的呼叫图鼠的巢。黄色是热路,红色是热点。

enter image description here

这表明多才多艺的加速机会在这些地方都不容易:

enter image description here

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

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

  • 3/10个样本正在调用class_exists,一个用于获取类名,另外两个用于设置本地配置。 class_exists调用调用autoload的{​​{1}},其中两个调用requireFile。如果这可以更直接地完成,它可以节省大约30%。
  • 2/10个样本正在调用adminpanel,调用determineId调用fetch_the_id,调用getPageAndRootlineWithDomain,调用另外三个级别,终止于sql_fetch_assoc。获得一个ID似乎很麻烦,而且它花费了大约20%的时间,并且不计算I / O.

因此,堆栈样本不会告诉您功能或代码行花费多少包容时间,它们会告诉您它为什么要完成,以及实现它需要多大的愚蠢。 我经常看到这一点 - 奔腾的普遍性 - 用锤子拍苍蝇,不是故意的,而只是遵循良好的模块化设计。

补充:另一件不被吸引的是火焰图。 例如,这里是来自上面的调用图的十个模拟堆栈样本的火焰图(向右旋转90度)。例程都是编号的,而不是命名的,但每个例程都有自己的颜色。 enter image description here
注意我们上面提到的问题, class_exists (例程219)在30%的样本上,通过查看火焰图并不是很明显。 更多的样本和不同的颜色会使图形看起来更像“火焰”和#34;但是不会暴露通过从不同的地方多次调用而花费大量时间的例程。

这里是按功能而不是按时间排序的相同数据。 这有点帮助,但不会聚集来自不同地方的相似性: enter image description here
再一次,目标是找到隐藏在你身上的问题。 任何人都可以找到容易的东西,但隐藏的问题是那些能够产生重大影响的问题。

补充:另一种眼睛是这一个:
enter image description here 黑色概述的例程可能都是相同的,只是从不同的地方调用。 该图不会为您聚合它们。 如果一个例程具有较高的包含百分比,可以从不同的地方多次调用,则不会暴露它。