协慌网

登录 贡献 社区

如何分析在 Linux 上运行的 C ++ 代码?

我有一个在 Linux 上运行的 C ++ 应用程序,我正在优化它。如何确定代码的哪些区域运行缓慢?

答案

如果您的目标是使用分析器,请使用其中一个建议器。

但是,如果你赶时间并且你可以在调试器下手动中断你的程序,而它主观上很慢,那么有一种简单的方法可以找到性能问题。

只需暂停几次,每次都看看调用堆栈。如果有一些代码浪费了一定比例的时间,20%或 50%或其他什么,那就是你在每个样本的行为中捕获它的概率。所以这大约是您将看到它的样本的百分比。没有必要的教育猜测。如果您确实猜测了问题是什么,这将证明或反驳它。

您可能有多个不同大小的性能问题。如果你清除其中任何一个,剩下的将占用更大的百分比,并且在随后的传球中更容易被发现。当在多个问题上复合时,这种放大效应可以导致真正大规模的加速因子。

警告:程序员往往对这种技术持怀疑态度,除非他们自己使用它。他们会说分析器会给你这些信息,但只有当他们对整个调用堆栈进行采样时才会这样,然后让你检查一组随机的样本。 (摘要是失去洞察力的地方。)调用图不会给你相同的信息,因为

  1. 他们没有在教学层面总结,并且
  2. 它们在递归的情况下给出令人困惑的摘要。

他们还会说它只适用于玩具程序,实际上它适用于任何程序,并且它似乎在更大的程序上更好地工作,因为它们往往有更多的问题要找。他们会说它有时会发现不是问题的东西,但只有在你看到一次之后才会这样。如果您在多个样本上发现问题,那就是真实的。

PS 如果有一种方法可以在某个时间点收集线程池的调用堆栈样本,那么这也可以在多线程程序上完成,就像在 Java 中一样。

PPS 作为一个粗略的概括,您在软件中拥有的抽象层越多,您就越有可能发现这是性能问题的原因(以及获得加速的机会)。

补充:它可能不是很明显,但堆栈采样技术在递归的情况下同样有效。原因是通过删除指令节省的时间通过包含它的样本的分数来近似,而不管样本中可能出现的次数。

我经常听到的另一个反对意见是:“ 它会在某个地方随机停止,它会错过真正的问题 ”。这来自对现实问题的先验概念。性能问题的一个关键属性是他们无视期望。抽样告诉你一些问题,你的第一反应是难以置信。这很自然,但你可以确定它是否发现问题是真的,反之亦然。

补充:让我对其工作原理进行贝叶斯解释。假设有一些指令I (调用或其他)在调用堆栈上的某个时间f (因此成本太高)。为简单起见,假设我们不知道f是什么,但假设它是 0.1,0.2,0.3,...... 0.9,1.0,并且这些可能性的先验概率为 0.1,因此所有这些成本都是相等的可能是先验的。

然后假设我们只采用 2 个堆栈样本,并且我们在两个样本上看到指令I ,指定观察o=2/2 。这给了我们对I的频率f的新估计,根据这个:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&&f=x)  P(o=2/2&&f >= x)  P(f >= x)

0.1    1     1             0.1          0.1            0.25974026
0.1    0.9   0.81          0.081        0.181          0.47012987
0.1    0.8   0.64          0.064        0.245          0.636363636
0.1    0.7   0.49          0.049        0.294          0.763636364
0.1    0.6   0.36          0.036        0.33           0.857142857
0.1    0.5   0.25          0.025        0.355          0.922077922
0.1    0.4   0.16          0.016        0.371          0.963636364
0.1    0.3   0.09          0.009        0.38           0.987012987
0.1    0.2   0.04          0.004        0.384          0.997402597
0.1    0.1   0.01          0.001        0.385          1

                  P(o=2/2) 0.385

最后一栏说,例如, f > = 0.5 的概率为 92%,高于之前假设的 60%。

假设先前的假设是不同的。假设我们假设 P(f = 0.1)是. 991(几乎确定),并且所有其他可能性几乎是不可能的(0.001)。换句话说,我们事先确定的是I很便宜。然后我们得到:

Prior                                    
P(f=x) x  P(o=2/2|f=x) P(o=2/2&& f=x)  P(o=2/2&&f >= x)  P(f >= x)

0.001  1    1              0.001        0.001          0.072727273
0.001  0.9  0.81           0.00081      0.00181        0.131636364
0.001  0.8  0.64           0.00064      0.00245        0.178181818
0.001  0.7  0.49           0.00049      0.00294        0.213818182
0.001  0.6  0.36           0.00036      0.0033         0.24
0.001  0.5  0.25           0.00025      0.00355        0.258181818
0.001  0.4  0.16           0.00016      0.00371        0.269818182
0.001  0.3  0.09           0.00009      0.0038         0.276363636
0.001  0.2  0.04           0.00004      0.00384        0.279272727
0.991  0.1  0.01           0.00991      0.01375        1

                  P(o=2/2) 0.01375

现在它说 P(f> = 0.5)是 26%,高于先前假设的 0.6%。所以贝叶斯允许我们更新我对I的可能成本的估计。如果数据量很小,它并不能准确地告诉我们成本是多少,只是它足够大,值得修复。

另一种看待它的方法叫做继承规则 。如果你将硬币翻了 2 次,并且两次都出现了硬币,那么它对硬币的可能加权有什么影响呢?值得尊重的回答方式是说它是 Beta 分布,平均值(命中数 + 1)/(尝试次数 + 2)=(2 + 1)/(2 + 2)= 75%。

(关键是我们不止一次看到I 。如果我们只看到一次,除了f > 0 之外,这并没有告诉我们多少。)

因此,即使是极少数的样本也能告诉我们很多关于它所看到的指令成本的信息。 (并且它将以平均频率与它们的成本成比例地看到它们。如果取n样本,并且f是成本,那么I将出现在nf+/-sqrt(nf(1-f))样本上。 , n=10f=0.3 ,即3+/-1.4样本。)


添加,以直观地感受测量和随机堆栈采样之间的差异:
现在有一些分析器可以对堆栈进行采样,即使是在挂钟时间,但是出现的是测量(或热路径,或热点,“瓶颈” 可以轻易隐藏)。他们没有告诉你(他们很容易)你自己的实际样本。如果您的目标是找到瓶颈,那么您需要查看的数量平均为 2 除以所需的时间。因此,如果需要 30%的时间,平均而言,2 / .3 = 6.7 个样本将显示它,并且 20 个样本将显示它的机会为 99.2%。

以下是检查测量和检查堆叠样本之间差异的袖口图示。瓶颈可能是这样的一个大块,或许多小块,它没有任何区别。

在此处输入图像描述

测量是水平的; 它告诉你特定例程所花费的时间。采样是垂直的。如果有任何方法可以避免整个程序在那一刻所做的事情, 如果你在第二个样本上看到它 ,你就找到了瓶颈。这就是产生差异的原因 - 看到花费时间的全部原因,而不仅仅是花费多少。

您可以使用Valgrind以下选项

valgrind --tool=callgrind ./(Your binary)

它将生成一个名为callgrind.out.x的文件。然后,您可以使用kcachegrind工具读取此文件。它会给你一个图形分析的结果,比如哪条线的成本是多少。

我假设你正在使用 GCC。标准解决方案是使用gprof 进行分析

确保在分析之前将-pg添加到编译中:

cc -o myprog myprog.c utils.c -g -pg

我还没有尝试过,但我听说过google-perftools 的消息 。绝对值得一试。

相关问题在这里

如果gprof没有为你完成这项工作,还有一些其他流行语: Valgrind ,Intel VTune ,Sun DTrace