如何找到在Linux上运行的C++应用程序中运行缓慢的代码区域?
当前回答
如果您的目标是使用探查器,请使用建议的探查器之一。
然而,如果您很匆忙,并且可以在调试器下手动中断程序,而程序主观上很慢,那么有一种简单的方法可以找到性能问题。
只需暂停几次,每次都查看调用堆栈。如果有一些代码浪费了一定百分比的时间,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%。
以下是检查测量值和检查堆栈样本之间的差异的即兴说明。瓶颈可能是一个像这样的大斑点,也可能是无数个小斑点,这没有什么区别。
测量是水平的;它告诉你特定时间的例行程序需要多少时间。采样是垂直的。如果有任何方法可以避免整个程序在那一刻所做的事情,并且如果您在第二个示例中看到了这一点,那么您就发现了瓶颈。这就是造成差异的原因——看到花费时间的全部原因,而不仅仅是花费多少。
其他回答
这是对Nazgob Gprof回答的回应。
过去几天我一直在使用Gprof,已经发现了三个重要的限制,其中一个是我在其他地方还没有看到过的:
它不能在多线程代码上正常工作,除非您使用变通方法调用图被函数指针弄糊涂了。示例:我有一个名为multithread()的函数,它使我能够在指定的数组上对指定的函数进行多线程处理(两者都作为参数传递)。然而,Gprof将所有对多线程()的调用视为等效的,以计算在孩子身上花费的时间。由于我传递给多线程()的一些函数花费的时间比其他函数长得多,所以我的调用图基本上是无用的。(对于那些想知道线程是否是这里的问题的人来说:不,多线程()可以选择,在这种情况下,只在调用线程上按顺序运行所有内容)。这里说“……调用数数字是通过计数而不是采样得出的。它们是完全准确的……”。然而,我发现我的调用图给了我5345859132+784984078作为对我调用最多的函数的调用统计数据,其中第一个数字应该是直接调用,第二个递归调用(都来自它本身)。因为这意味着我有一个bug,所以我在代码中加入了长(64位)计数器,并再次运行相同的程序。我的计数:5345859132个直接调用和78094395406个自递归调用。这里有很多数字,所以我要指出,我测量的递归调用是780亿,而Gprof是7.84亿:相差100倍。两次运行都是单线程和未优化的代码,一次是编译的-g,另一次是-pg。
这是在64位Debian Lenny下运行的GNUGprof(Debian的GNUBinutils)2.18.0.20080103,如果这对任何人都有帮助的话。
较新的内核(例如最新的Ubuntu内核)附带了新的“perf”工具(apt-get-install-linux-tools)AKA perf_events。
这些都配有经典的采样分析器(手册页)以及很棒的时间图表!
重要的是,这些工具可以是系统评测,而不仅仅是进程评测-它们可以显示线程、进程和内核之间的交互,并让您了解进程之间的调度和I/O依赖关系。
使用调试软件如何识别代码运行缓慢的地方?
如果你在运动中遇到障碍物,那么它会降低你的速度
如不需要的重新分配的循环、缓冲区溢出、搜索、内存泄漏等操作消耗更多的执行能力,这将对代码的性能产生不利影响,在分析之前,请确保将-pg添加到编译中:
g++your_prg.cpp-pg或cc my_program.cpp-g-pg(根据编译器)
我还没有尝试过,但我听到了关于谷歌perftools的好消息。这绝对值得一试。
valgrind--tool=callgrind/(二进制文件)
它将生成一个名为gmon.out或callgrind.out.x的文件。然后可以使用kcachegrind或调试器工具来读取该文件。它会给你一个图形化的分析结果,比如哪一行花费多少。
我认为是这样
事实上,没有多少人提到google/基准测试,这有点让人惊讶,虽然固定代码的特定区域有点麻烦,特别是如果代码库有点大的话,但是我发现这在与callgrind结合使用时非常有用
IMHO识别导致瓶颈的工件是这里的关键。不过,我会先尝试回答以下问题,然后根据这些问题选择工具
我的算法正确吗?有锁被证明是瓶颈吗?是否有一段特定的代码被证明是罪魁祸首?IO如何处理和优化?
valgrind与callgrind和kcachegrind的结合应该能对以上几点提供一个不错的估计,一旦确定某段代码存在问题,我建议做一个微基准测试——谷歌基准测试是一个很好的开始。
对于单线程程序,您可以使用igprof,The Ignorminous Profiler:https://igprof.org/ .
这是一个采样分析器,沿着。。。长的Mike Dunlavey的回答,它将把结果包装在一个可浏览的调用堆栈树中,用每个函数(无论是累积的还是每个函数)花费的时间或内存进行注释。