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


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


1499
2017-12-17 20:29


起源


如果您将提供有关开发堆栈的更多数据,您可能会得到更好的答案。有英特尔和Sun的分析器,但你必须使用他们的编译器。这是一个选择吗? - Nazgob
已在以下链接中回答: stackoverflow.com/questions/2497211/... - Kapil Gupta
大多数答案都是 code 剖析。但是,优先级倒置,缓存别名,资源争用等都可以成为优化和性能的因素。我认为人们会把信息读进去 我的慢代码。常见问题解答引用此主题。 - artless noise
CppCon 2015:Chandler Carruth“调优C ++:基准测试,CPU和编译器!哦,我的!” - 865719
我曾经随机使用pstack,大多数时候会打印出大多数时间程序最典型的堆栈,因此指向了瓶颈。 - Jose Manuel Gomez Alvarez


答案:


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

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

只需暂停几次,每次都看看调用堆栈。如果有一些代码浪费了一定比例的时间,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。这为我们提供了频率的新估计 f 的 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.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%。

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

enter image description here

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


1194
2018-04-21 04:09



这基本上是一个穷人的采样分析器,这很好,但你冒的样本量太小的风险可能会给你完全虚假的结果。 - Crashworks
@Crash:我不会讨论“穷人”部分:-)统计测量精度确实需要很多样本,但有两个相互冲突的目标 - 测量和问题定位。我专注于后者,你需要精确的位置,而不是精确的测量。例如,中间堆栈可以有单个函数调用A();占50%的时间,但它可以在另一个大型函数B中,以及对A()的许多其他调用并不昂贵。功能时间的精确总结可能是一个线索,但每个其他堆栈样本将查明问题。 - Mike Dunlavey
......世界似乎认为用呼叫计数和/或平均时间注释的呼叫图就足够了。不是这样。而令人遗憾的是,对于那些对调用堆栈进行抽样的人来说,最有用的信息就在他们面前,但为了“统计”的利益,他们把它扔掉了。 - Mike Dunlavey
我不是故意不同意你的技巧。很明显,我非常依赖堆栈行走采样分析器。我只是指出现在有一些工具以自动方式完成,这一点非常重要,当你将函数从25%提高到15%并需要将其从1.2%降低到0.6%。 - Crashworks
非常感谢你的想法。我刚刚使用它,我能够比过去尝试的任何其他方法更快地识别和改善一些严重的瓶颈。我加快执行60次。考虑到我正在考虑添加的所有定时调试代码,我感到不寒而栗。 - ErikE


您可以使用 Valgrind的 有以下选项

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

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


472
2017-12-17 20:34



valgrind很棒,但要注意它会使你的程序变慢 - neves
退房也 Gprof2Dot 用于可视化输出的惊人替代方法。 ./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg - Sebastian
@neves是Valgrind在实时分析“gstreamer”和“opencv”应用程序的速度方面不是很有帮助。 - enthusiasticgeek
stackoverflow.com/questions/375913/... 是速度问题的部分解决方案。 - Tõnu Samuel
@Sebastian: gprof2dot 现在在这里: github.com/jrfonseca/gprof2dot - John Zwinck


我假设你正在使用GCC。标准解决方案是配置文件 gprof的

一定要添加 -pg 在分析之前进行编译:

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

我还没试过,但我听说过好话 谷歌,perftools。绝对值得一试。

相关问题 这里

其他一些流行语如果 gprof 不为你做的工作: Valgrind的,英特尔 VTune™可视化,太阳 DTrace的


296
2017-08-17 11:48



我同意gprof是现行标准。但请注意,Valgrind用于描述程序的内存泄漏和其他与内存相关的方面,而不是速度优化。 - Bill the Lizard
比尔,在vaglrind套房,你可以找到callgrind和massif。两者对于分析应用程序非常有用 - dario minonne
@ Bill-the-Lizard:一些评论 gprof的 : stackoverflow.com/questions/1777556/alternatives-to-gprof/... - Mike Dunlavey
另见下面我的gprof告诫, stackoverflow.com/a/6540100/823636 - Rob_before_edits
gprof -pg只是callstack分析的近似值。它插入mcount调用来跟踪哪些函数正在调用哪些函数。它使用标准的基于时间的采样,呃,时间。然后它将在函数foo()中采样的时间分配给foo()的调用者,以支持调用的数量。因此它不区分不同成本的呼叫。 - Krazy Glew


较新的内核(例如最新的Ubuntu内核)附带了新的'perf'工具(apt-get install linux-tools)AKA perf_events

这些配有经典的采样分析仪(手册页)以及令人敬畏的 时间图

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

Alt text


216
2018-05-22 21:44



伟大的工具!无论如何我有一个典型的“蝴蝶”视图,从“main-> func1-> fun2”风格开始?我似乎无法弄明白...... perf report似乎给我带有调用父母的函数名称...(所以它是一种倒置的蝴蝶视图) - kizzx2
Will,可以显示线程活动的时间表;添加了CPU编号信息?我想看看每个CPU上运行的时间和线程。 - osgx
@ kizzx2 - 你可以使用 gprof2dot 和 perf script。非常好的工具! - dashesy
即使像4.13这样的新内核也有eBPF用于分析。看到 brendangregg.com/blog/2015-05-15/ebpf-one-small-step.html 和 brendangregg.com/ebpf.html - Andrew Stern
另一个很好的介绍 perf 存在于 archive.li/9r927#selection-767.126-767.271   (为什么SO众神决定从SO知识库中删除该页面超出了我的范围......) - ragerdl


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

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

Callgrind是一个构建于此的探查器。主要好处是您不必花费数小时来运行您的应用程序以获得可靠的结果。即使是第二次运行也足以获得坚如磐石,可靠的结果,因为Callgrind是一个 非探测 探查。

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


65
2018-06-30 19:30





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

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

  1. 它在多线程代码上无法正常工作,除非您使用 解决方法

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

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

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


49
2018-06-08 08:01



显然它可以做抽样 stackoverflow.com/a/11143125/32453 - rogerdpack
是的,它会进行抽样,但不适用于号码数字。有趣的是,关注你的链接最终导致我在我的帖子中链接到的手册页的更新版本,新网址: sourceware.org/binutils/docs/gprof/...  这重复了我的答案的第(iii)部分中的引用,但也说“在多线程应用程序或与多线程库链接的单线程应用程序中,如果计数功能是线程安全的,则计数只是确定性的。”注意:请注意glibc中的mcount计数功能不是线程安全的。)“ - Rob_before_edits
我不清楚这是否解释了我在(iii)中的结果。我的代码链接了-lpthread -lm并声明了“pthread_t * thr”和“pthread_mutex_t nextLock = PTHREAD_MUTEX_INITIALIZER”静态变量,即使它正在运行单线程。我通常会认为“与多线程库链接”意味着实际使用这些库,并且在更大程度上,但我可能是错的! - Rob_before_edits


跑步的答案 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()”是最耗时的任务。 “自我”显示每个功能本身需要多少时间,而不是与家属一起。


47
2018-02-23 21:28



现在因为某些原因,callgrind.out。*文件总是空的。执行callgrind_control -d对于强制将数据转储到磁盘很有用。 - Tõnu Samuel
我对这些说明感到困惑。你是这么说的吗? 程序运行时,我们可以执行 callgrind_control 在另一个窗口打开/关闭分析?在我看来,设计一个仅包含您想要分析的内容的最小程序,然后分析整个程序会更好。 - dbliss
不能。我通常的上下文类似于整个MySQL或PHP或类似的大事。通常甚至不知道我想先分开什么。 - Tõnu Samuel
或者在我的情况下,我的程序实际上将一堆数据加载到LRU缓存中,我想不要对其进行分析。因此,我在启动时强制加载缓存的子集,并仅使用该数据来分析代码(让OS + CPU管理缓存中的内存使用)。它可以正常工作,但加载该缓存的速度很慢,而且我正在尝试在不同的上下文中分析代码的CPU密集,因此callgrind会产生严重污染的结果。 - Code Abominator
还有 CALLGRIND_TOGGLE_COLLECT 以编程方式启用/禁用收集;看到 stackoverflow.com/a/13700817/288875 - Andre Holzner


使用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
2018-03-17 12:20



我不同意你的回答,但我同意你的历史 - 工程,CS,研究生学位和高级教学。祝你好运。 - Mike Dunlavey


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

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


4
2017-11-28 18:21





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

对于CPU绑定应用程序:

  1. 在DEBUG模式下使用分析器来识别代码中可疑的部分
  2. 然后切换到RELEASE模式并注释掉代码中可疑的部分(没有任何内容),直到看到性能发生变化。

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

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

注:

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

对于CPU,分析的原因 DEBUG 模式是因为如果您尝试过分析 发布 模式,编译器将减少数学,向量化循环和内联函数,这些函数往往会使代码在组装时变成不可映射的混乱。 不可映射的混乱意味着您的探查器将无法清楚地识别所需的时间,因为程序集可能与优化下的源代码不对应。如果你需要性能(例如时间敏感) 发布 模式,根据需要禁用调试器功能以保持可用性能。

对于I / O绑定,探查器仍然可以识别I / O操作 发布 模式,因为I / O操作要么外部链接到共享库(大多数时候),要么在最坏的情况下,将导致sys-call中断向量(分析器也很容易识别)。


2



+1穷人的方法对于I / O绑定和CPU绑定一样有效,我建议在DEBUG模式下进行所有性能调整。完成调整后,启用RELEASE。如果程序在代码中受CPU限制,它将进行改进。 这是一个粗略但简短的过程视频。 - Mike Dunlavey
我不会使用DEBUG构建进行性能分析。我经常看到DEBUG模式下的性能关键部件在发布模式下完全优化。另一个问题是在调试代码中使用断言会给性能增加噪声。 - gast128
你读过我的帖子了吗? “如果您需要RELEASE模式的性能(例如时序敏感),请根据需要禁用调试器功能以保持可用的性能”,然后切换到RELEASE模式并注释代码的可疑部分(无任何存根)直到看到性能的变化。“?我说在调试模式下检查可能的问题区域并在发布模式下验证这些问题以避免您提到的陷阱。 - seo