使用 __gnu_mcount_nc 捕获函数退出时间

2024-02-09

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

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

我真的很想获得有关函数体中花费的时间的信息,但是我很难理解如何仅通过入口而不是出口来处理每个函数被钩住的问题:您可以准确地告诉每个函数何时输入,但如果不挂钩退出点,您将无法知道在多少时间内收到下一条信息以归因于被调用者,以及有多少时间归因于调用者。

尽管如此,GNU 分析工具实际上显然能够收集许多平台上函数的运行时信息,因此想必开发人员已经想到了一些实现这一目标的方案。

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

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

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

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


gprof不使用该功能进行计时、输入orexit,但用于对函数 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 个样本。 函数调用是否短也没关系——如果它们被调用的次数足够多而花费了很大一部分,它们就会被捕获。)

请注意,当您寻找可以修复的问题以提高速度时,确切的百分比并不重要。 重要的是找到它。 (其实你只需要看到一个问题twice要知道它足够大,可以修复。)

That's 这项技术 https://stackoverflow.com/a/378024/23771.


附:不要陷入调用图、热路径或热点中。 这是一个典型的调用图老鼠巢。黄色是热点路径,红色是热点。

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

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

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

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

因此,堆栈示例不仅告诉您一个函数或一行代码花费了多少包含时间,还告诉您为什么要这样做,以及完成它可能需要哪些愚蠢的事情。 我经常看到这种现象——奔腾的普遍性——用锤子打苍蝇,不是故意的,只是遵循良好的模块化设计。

ADDED: Another thing not to get sucked into is flame graphs. For example, here is a flame graph (rotated right 90 degrees) of the ten simulated stack samples from the call graph above. The routines are all numbered, rather than named, but each routine has its own color. enter image description here
Notice the problem we identified above, with class_exists (routine 219) being on 30% of the samples, is not at all obvious by looking at the flame graph. More samples and different colors would make the graph look more "flame-like", but does not expose routines which take a lot of time by being called many times from different places.

Here's the same data sorted by function rather than by time. That helps a little, but doesn't aggregate similarities called from different places: enter image description here
Once again, the goal is to find the problems that are hiding from you. Anyone can find the easy stuff, but the problems that are hiding are the ones that make all the difference.

ADDED: Another kind of eye-candy is this one:
enter image description here where the black-outlined routines could all be the same, just called from different places. The diagram doesn't aggregate them for you. If a routine has high inclusive percent by being called a large number of times from different places, it will not be exposed.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 __gnu_mcount_nc 捕获函数退出时间 的相关文章

随机推荐