c++ - 使用__gnu_mcount_nc捕获函数退出时间

标签 c++ gcc profiling gprof

我正在尝试在一个缺乏良好支持的原型(prototype)嵌入式平台上进行性能分析。

我注意到,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不使用该函数计时,进入或退出,而是用于函数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

    P.S.不要陷入电话图,热路径或热点中。
    这是典型的电话图老鼠的巢。黄色是热点,红色是热点。

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

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

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

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

  • 因此,堆栈样本不仅告诉您函数或一行代码要花多少时间(包含时间),还告诉您为什么要执行此操作以及执行此操作可能需要采取何种愚蠢措施。
    我经常看到这一点-疾驰一般-用锤子扑打苍蝇,不是故意的,而是遵循良好的模块化设计。

    添加:另一个不可吸入的东西是火焰图。
    例如,这是来自上面调用图的十个模拟堆栈样本的火焰图(向右旋转90度)。例程全部编号,而不是命名,但是每个例程都有其自己的颜色。

    请注意,我们在上面确定的问题(class_exists)(例程219)在30%的样本上,通过查看火焰图根本看不出来。
    更多的样本和不同的颜色将使图形看起来更像“火焰状”,但不会暴露从不同地方被多次调用而花费大量时间的例程。

    这是按功能而不是时间排序的相同数据。
    这会有所帮助,但不会汇总来自不同地方的相似之处:

    再一次,目标是找到隐藏在您面前的问题。
    任何人都可以找到简单的东西,但是隐藏起来的问题却是使一切都不同的问题。

    补充:另一种眼神糖果是:

    黑色轮廓线的例程可能都是相同的,只是从不同的地方调用而已。
    该图不会为您汇总它们。
    如果例程从不同的地方被多次调用而具有较高的包含率,则该例程将不会被公开。

    关于c++ - 使用__gnu_mcount_nc捕获函数退出时间,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25865264/

    相关文章:

    c++代码结构到html文件

    c - 如何在c中打印文件的内容?

    javascript - 在 chrome 中分析 JavaScript?

    java - 如何分析 native JNI 库

    c++ - 碱基转换问题

    c++ - 程序在抛出异常时挂起

    c++ - 在模板函数中调用继承的方法

    c++ - gcc 和 libstdc++ 向前兼容

    c - pthread_create() 的返回码是 11

    .Net Profiling API 方法