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% 的样本上,通过查看火焰图一点也不明显。
更多的样本和不同的颜色会使图形看起来更像“火焰”,但不会暴露从不同地方多次调用而需要大量时间的例程。
这是按功能而不是按时间排序的相同数据。
这有点帮助,但不会汇总从不同地方调用的相似性:
再一次,目标是找到对你隐藏的问题。
任何人都可以找到简单的东西,但隐藏的问题才是最重要的。
添加:另一种眼睛糖果是这个:
黑色轮廓的例程可能都是相同的,只是从不同的地方调用。
该图不会为您汇总它们。
如果一个套路在不同地方被大量调用,具有很高的包容率,它就不会被暴露。