提问



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

最佳参考


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


但是,如果你很匆忙,并且你可以手动中断调试器下的程序,而主观速度很慢,那么就有一种简单的方法可以找到性能问题。


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


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


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



  1. 他们不会在教学层面总结,

  2. 他们在递归的情况下给出了令人困惑的摘要。



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


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


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


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


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


补充:让我对其工作原理进行贝叶斯解释。假设有一些指令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%。[[[36]


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


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





添加,以直观地感受测量和随机堆栈采样之间的差异:

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


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


[37]


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

其它参考1


您可以使用Valgrind以下选项[38]


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


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

其它参考2


我假设您正在使用GCC。标准解决方案是使用gprof进行配置。[39]


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


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


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


相关问题在这里。


如果gprof不适合你的话,还有一些流行语:Valgrind,Intel VTune,Sun DTrace。[42] [43] [44]

其它参考3


较新的内核(例如最新的Ubuntu内核)带有新的perf工具(apt-get install linux-tools)AKA perf_events。[45]


这些带有经典的采样分析器(手册页)以及令人敬畏的时间表![46] [47]


重要的是这些工具可以是系统概要分析而不仅仅是进程概要分析 - 它们可以显示线程,进程和内核之间的交互,让您了解进程之间的调度和I/O依赖性。




其它参考4


我会使用Valgrind和Callgrind作为我的分析工具套件的基础。重要的是要知道Valgrind基本上是一个虚拟机:



  (维基百科)Valgrind本质上是虚拟的
  使用即时(JIT)的机器
  编译技术,包括
  动态重新编译。没什么
  原始程序运行
  直接在主处理器上。
  相反,Valgrind首先翻译了
  程序成一个临时的,简单的形式
  称为中间代表
  (IR),处理器中立,
  基于SSA的表格。转换后,
  一个工具(见下文)是免费的
  无论转变什么
  在Valgrind翻译之前,在IR上
  IR回到机器代码并让
  主处理器运行它。



Callgrind是一个构建于此的探查器。主要好处是你不必运行你的应用程序几个小时才能获得可靠的结果。即使一秒钟运行也足以获得坚如磐石,可靠的结果,因为Callgrind是一个非探测分析器。


建立在Valgrind上的另一个工具是Massif。我用它来分析堆内存使用情况。它很棒。它的作用是为你提供内存使用的快照 - 详细信息什么是内存百分比,以及世界卫生组织把它放在那里。此类信息可在应用程序运行的不同时间点获得。

其它参考5


这是对Nazgob的Gprof答案的回应。


我过去几天一直在使用Gprof,并且已经发现了三个重要的限制,其中一个我还没有在其他任何地方看到过记录:



  1. 除非您使用变通方法[49],否则它在多线程代码上无法正常工作

  2. 调用图被函数指针搞糊涂了。示例:我有一个名为multithread()的函数,它使我能够在指定的数组上多线程化指定的函数(两者都作为参数传递)。但是,Gprof将所有对多线程()的调用视为等效的,以便计算在子节点上花费的时间。由于我传递给multithread()的一些函数比其他函数花费的时间长得多,因此我的调用图几乎没用。 (对于那些想知道线程是否是问题的人:不,multithread()可以选择,并且在这种情况下,只在调用线程上顺序运行所有内容)。

  3. 这里说的是......通过计数而不是采样来得出呼叫数字。它们完全准确......。然而我发现我的调用图给了我5345859132 + 784984078作为我最常调用函数的调用统计数据,其中第一个数字应该是直接调用,第二个递归调用(它们都来自自身)。由于这暗示我有一个错误,我将长(64位)计数器放入代码并再次执行相同的操作。我的计数:5345859132直接和78094395406自我递归调用。那里有很多数字,所以我会指出我测量的递归调用是780亿,而Gprof是784m:100个不同的因素。两个运行都是单线程和未优化的代码,一个编译-g和另一个 - PG [50]



这是在64位Debian Lenny下运行的GNU Gprof(GNU Binutils for Debian)2.18.0.20080103,如果这有助于任何人。[51]

其它参考6


没有一些选项,运行valgrind --tool=callgrind的答案并不完全。我们通常不想在Valgrind下配置10分钟的慢启动时间,并且想要在执行某项任务时对我们的程序进行分析。


所以这就是我的建议。首先运行程序:


valgrind --tool=callgrind --dump-instr=yes -v --instr-atstart=no ./binary > tmp


现在当它工作并且我们想要开始分析时,我们应该在另一个窗口中运行:


callgrind_control -i on


这会打开分析。要关闭它并停止整个任务,我们可能会使用:


callgrind_control -k


现在我们在当前目录中有一些名为callgrind.out。*的文件。要查看分析结果,请使用:


kcachegrind callgrind.out.*


我建议在下一个窗口中单击Self列标题,否则它会显示main()是最耗时的任务。 自我显示每个功能本身需要多少时间,而不是与家属一起。

其它参考7


使用Valgrind,callgrind和kcachegrind:


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


生成callgrind.out.x。使用kcachegrind读取它。


使用gprof(添加-pg):


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


(不太适合多线程,函数指针)


使用google-perftools:


使用时间采样,揭示I/O和CPU瓶颈。


英特尔VTune是最好的(免费用于教育目的)。


其他人: AMD Codeanalyst,OProfile,perf工具(apt-get install linux-tools)

其它参考8


对于单线程程序,您可以使用 igprof ,Ignominous Profiler:https://igprof.org/。[52]


它是一个采样分析器,沿着......很长的答案由Mike Dunlavey回答,它将把结果包装在一个可浏览的调用堆栈树中,用每个函数花费的时间或内存注释,累加或每个函数。

其它参考9


这些是我用来加速代码的两种方法:


对于CPU绑定应用程序:



  1. 在DEBUG模式下使用分析器来识别代码的可疑部分

  2. 然后切换到RELEASE模式并注释掉代码中可疑的部分(没有任何内容),直到看到性能发生变化为止。



对于I/O绑定的应用程序:



  1. 在RELEASE模式下使用分析器来识别代码的可疑部分。






注:


如果你没有探查器,请使用穷人的探查器。在调试应用程序时按下暂停。大多数开发人员套件都会使用注释行号进入汇编。从统计上来说,您可能会进入占用大部分CPU周期的区域。


对于CPU,在 DEBUG 模式下进行性能分析的原因是因为如果您尝试在 RELEASE 模式下进行性能分析,编译器将减少数学,向量化循环和内联函数将你的代码组合成一个不可映射的混乱组件。一个不可映射的混乱意味着你的分析器将无法清楚地识别出这么长时间的内容,因为程序集可能与下面的源代码不对应优化。如果您需要 RELEASE 模式的性能(例如时序敏感),请根据需要禁用调试器功能以保持可用性能。


对于I/O绑定,探查器仍然可以在 RELEASE 模式下识别I/O操作,因为I/O操作要么外部链接到共享库(大多数时候),要么在最坏的情况下,将导致系统调用中断向量(分析器也可以轻松识别)。