如何找到在Linux上运行的C++应用程序中运行缓慢的代码区域?


当前回答

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

这是一个采样分析器,沿着。。。长的Mike Dunlavey的回答,它将把结果包装在一个可浏览的调用堆栈树中,用每个函数(无论是累积的还是每个函数)花费的时间或内存进行注释。

其他回答

使用Valgrind、callgrind和kcachegrind:

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

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

使用gprof(add-pg):

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

(对于多线程、函数指针不太好)

使用google perftools:

使用时间采样,可以发现I/O和CPU瓶颈。

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

其他:AMD Codeanalysis(已被AMD CodeXL取代)、OProfile、“perf”工具(apt-get-install-linux工具)

如果您的目标是使用探查器,请使用建议的探查器之一。

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

只需暂停几次,每次都查看调用堆栈。如果有一些代码浪费了一定百分比的时间,20%或50%或更多,这就是您在每个示例的操作中捕获它的概率。所以,这大概是你将看到的样本的百分比。不需要经过教育的猜测。如果你确实猜到了问题所在,这将证明或反驳它。

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

注意:除非程序员自己使用过,否则他们倾向于怀疑这种技术。他们会说,分析程序提供了这些信息,但只有当他们对整个调用堆栈进行采样,然后让您检查一组随机样本时,这才是正确的。(总结是失去洞察力的地方。)调用图不能提供相同的信息,因为

他们不在教学层面进行总结,而且在递归的情况下,它们给出了令人困惑的摘要。

他们也会说它只适用于玩具程序,而实际上它适用于任何程序,而且它似乎更适用于大型程序,因为他们往往会发现更多问题。他们会说,它有时会发现不存在问题的东西,但只有当你看到一次时,这才是真的。如果您在多个样本上看到问题,则这是真实的。

P.S.如果有一种方法可以在某个时间点收集线程池的调用堆栈样本,这也可以在多线程程序上完成,就像在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 | o=2/2)

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)。换句话说,我们先前的确定是我很便宜。然后我们得到:

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 | o=2/2)

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的可能成本的估计。如果数据量很小,它不能准确地告诉我们成本是多少,只是它足够大,值得修复。

另一种看待它的方式被称为继承规则。如果你把一枚硬币翻了两次,两次都是正面朝上,那么这能告诉你硬币的可能重量吗?令人尊敬的答案是,这是一个Beta分布,平均值(点击次数+1)/(尝试次数+2)=(2+1)/(2+2)=75%。

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

因此,即使是非常少量的样本也可以告诉我们它所看到的指令的成本。(它会看到它们的频率,平均来说,与它们的成本成正比。如果取n个样本,f是成本,那么我会出现在nf+/-sqrt(nf(1-f))样本上。例如,n=10,f=0.3,即3+/-1.4个样本。)


添加:为了直观地感受测量和随机堆栈采样之间的差异:现在有分析器可以对堆栈进行采样,即使是在墙上的时钟时间,但结果是测量结果(或热路径或热点,“瓶颈”很容易隐藏)。他们没有向你展示(而且很容易展示)的是实际样品本身。如果你的目标是找到瓶颈,那么你需要看到的瓶颈数量平均是2除以所需的时间。因此,如果花费30%的时间,平均来说,2/.3=6.7个样本将显示它,20个样本显示它的概率为99.2%。

以下是检查测量值和检查堆栈样本之间的差异的即兴说明。瓶颈可能是一个像这样的大斑点,也可能是无数个小斑点,这没有什么区别。

测量是水平的;它告诉你特定时间的例行程序需要多少时间。采样是垂直的。如果有任何方法可以避免整个程序在那一刻所做的事情,并且如果您在第二个示例中看到了这一点,那么您就发现了瓶颈。这就是造成差异的原因——看到花费时间的全部原因,而不仅仅是花费多少。

使用调试软件如何识别代码运行缓慢的地方?

如果你在运动中遇到障碍物,那么它会降低你的速度

如不需要的重新分配的循环、缓冲区溢出、搜索、内存泄漏等操作消耗更多的执行能力,这将对代码的性能产生不利影响,在分析之前,请确保将-pg添加到编译中:

g++your_prg.cpp-pg或cc my_program.cpp-g-pg(根据编译器)

我还没有尝试过,但我听到了关于谷歌perftools的好消息。这绝对值得一试。

valgrind--tool=callgrind/(二进制文件)

它将生成一个名为gmon.out或callgrind.out.x的文件。然后可以使用kcachegrind或调试器工具来读取该文件。它会给你一个图形化的分析结果,比如哪一行花费多少。

我认为是这样

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

这些都配有经典的采样分析器(手册页)以及很棒的时间图表!

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

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

对于CPU绑定的应用程序:

在DEBUG模式下使用探查器来识别代码中有问题的部分然后切换到RELEASE模式,注释掉代码中有问题的部分(不加任何内容),直到看到性能的变化。

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

在RELEASE模式下使用探查器来识别代码中有问题的部分。


N.B.

如果你没有剖析器,就用穷人的剖析器。调试应用程序时单击暂停。大多数开发人员套件将使用注释的行号分解成程序集。从统计上看,你很可能会在一个消耗了大部分CPU周期的区域着陆。

对于CPU来说,在DEBUG模式下进行评测的原因是,如果您尝试在RELEASE模式下进行剖析,编译器将减少数学、矢量化循环和内联函数,这些函数在汇编代码时会使代码陷入无法映射的混乱。无法映射的混乱意味着您的探查器将无法清楚地识别所需的时间,因为程序集可能与正在优化的源代码不符。如果您需要RELEASE模式的性能(例如,对时间敏感),请根据需要禁用调试器功能以保持可用的性能。

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