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

我有一个C ++应用程序,在Linux上运行,我正在优化。 我怎样才能确定我的代码的哪个区域运行缓慢?

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

但是,如果您急于在debugging器中手动中断程序,而主观上很慢,则可以通过简单的方法find性能问题。

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

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

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

  1. 他们不在教学层面进行总结
  2. 他们在recursion的情况下给出了令人困惑的总结。

他们也会说,它只适用于玩具程序,实际上它适用于任何程序,似乎在更大的程序上效果更好,因为它们往往会遇到更多的问题。 他们会说,有时会发现一些不是问题的东西,但是只有当你看到某种东西的时候才会这样。 如果你看到一个以上的样本有问题,这是真实的。

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

PPS简单来说,软件中抽象层次越多,发现性能问题的原因就越多(有机会获得加速)。

补充:可能不是很明显,但堆栈抽样技术在recursion的情况下工作得很好。 原因在于,通过删除指令可以节省的时间大概是包含它的样本的一小部分,而不pipe样本中可能发生的次数。

我经常听到的另一个反对意见是:“ 随意停止,这会错过真正的问题 ”。 这是由于事先有一个真正的问题的概念。 性能问题的一个重要特征是他们违背期望。 抽样告诉你什么是一个问题,你的第一反应是不相信。 这是很自然的,但是你可以肯定,如果它发现问题是真的,反之亦然。

增加:让我做一个贝叶斯解释它是如何工作的。 假设有一些指令I (呼叫或其他)在调用堆栈的一小部分时间(因此成本那么多)。 为了简单起见,假设我们不知道f是什么,但是假设它是0.1,0.2,0.3,… 0.9,1.0,并且每个这些可能性的先验概率是0.1,所以所有这些成本是平等的可能是先验的。

那么假设我们只取两堆样本,我们在两个样本上都看到指令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%。 所以贝叶斯允许我们更新我对可能成本的估计。 如果数据量很小,它并不能准确地告诉我们成本是多less,只是它足够大才值得修复。

另一种看待它的方式被称为inheritance规则 。 如果你把一个硬币翻了两次,而且这个硬币两次出现,那么这个硬币的重量是多less? 推荐的方法是说这是一个Beta分布,平均值(命中数+ 1)/(尝试次数+2)=(2 + 1)/(2 + 2)= 75%。

(关键是我们不止一次看到我们,如果我们只看到一次,除了f > 0之外没有多less意义)

因此,即使只有很less的样本可以告诉我们很多关于它看到的指令的成本。 (平均来看,它们的频率与其成本成正比,如果采用n样本, f是成本,那么I将出现在nf+/-sqrt(nf(1-f))样本上。 , n=10f=0.3 ,即3+/-1.4样品)。


ADDED,为测量和随机堆栈采样之间的差异提供直观的感受:
现在有一些探测器可以对栈进行采样,即使在挂钟时间,但是测量结果(或热path或热点,“瓶颈”可以很容易地隐藏起来)。 他们不显示你(他们很容易就可以)是他们自己的实际样本。 如果你的目标是find瓶颈,那么你需要看到的数量平均是2,除以所花费的时间。 所以如果花费30%的时间,2 / .3 = 6.7个样本平均会显示出来,而20个样本显示的几率是99.2%。

这里是检查测量和检查堆栈样本之间差异的一个非常好的例子。 瓶颈可能是这样的一个大块,或许多小块,这没有什么区别。

在这里输入图像描述

测量是水平的; 它会告诉你具体例程所花费的时间是多less。 采样是垂直的。 如果有什么方法可以避免整个程序在那个时候正在做什么, 并且如果你在第二个样本上看到它 ,你就发现了瓶颈。 这就是造成这种差异的原因 – 看看花费的全部理由,而不仅仅是多less。

您可以使用Valgrind以下选项

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

它将生成一个名为callgrind.out.x的文件。 然后你可以使用kcachegrind工具来读取这个文件。 它会给你一个结果的graphics分析,比如哪一行花了多less钱。

我假设你正在使用GCC。 标准的解决scheme是使用gprof进行configuration。

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

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

我还没有尝试过,但我听说过谷歌perftools好东西。 这绝对值得一试。

相关问题在这里 。

如果gprof不为你工作,还有一些其他的stream行语: Valgrind ,Intel VTune ,Sun DTrace 。

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

这些配备了经典的采样分析器( man-page )以及令人敬畏的时间表 !

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

替代文字

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

(wikipedia)Valgrind本质上是一个使用即时(JIT)编译技术(包括dynamic重新编译)的虚拟机。 原始程序中没有任何东西直接在主处理器上运行。 相反,Valgrind首先将该程序转换为一种称为中间表示(Intermediate Representation,IR)的临时简单forms,它是一种处理器中立,基于SSA的forms。 转换后,在Valgrind将IR转换回机器码并让主机处理器运行之前,一个工具(见下文)可以自由地在IR上进行任何转换。

Callgrind是一个build立在此基础上的分析器。 主要好处是你不必运行你的应用程序几个小时得到可靠的结果。 即使一秒钟的运行也足以得到稳定可靠的结果,因为Callgrind是一个非探测性的分析器。

Valgrind的另一个工具是Massif。 我用它来分析堆内存使用情况。 它工作很好。 它所做的就是为您提供内存使用的快照 – 详细信息什么是内存的百分比,WHO已经把它放在那里。 这些信息在应用程序运行的不同时间点可用。

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

我过去几天一直在使用Gprof,并已经发现了三个重要的限制,其中一个我还没有在其他地方见到(还没有):

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

  2. 调用图被函数指针弄糊涂了。 例子:我有一个名为multithread()的函数,它使我能够在指定的数组上multithreading指定的函数(都作为parameter passing)。 然而,Gprof将所有对multithread()的调用视为等同于计算在儿童中花费的时间。 由于我传递给multithread()的一些函数比其他函数花费的时间要长得多,所以我的调用图大多是无用的。 (对于那些想知道线程是否是问题的人,可以selectno,multithread(),在这种情况下,只能在调用线程上按顺序运行)。

  3. 它在这里说:“……通话数字是通过计数而不是取样得出的,它们是完全准确的……”。 然而,我发现我的通话图给我5345859132 + 784984078作为我最称为函数,其中第一个数字应该是直接调用,第二次recursion调用(都是从本身)的调用统计。 既然这意味着我有一个bug,我把长的(64位)计数器放入代码,并再次运行。 我的计数:5345859132直接和78094395406自我recursion调用。 在那里有很多数字,所以我会指出我测量的recursion调用是780亿,而Gprof则是784万:这是100个不同的因子。 这两个运行是单线程和未优化的代码,一个编译-g和另一个-pg。

这是在64位Debian Lenny下运行的GNU Gprof (GNU Binutils for Debian)2.18.0.20080103,如果有帮助的话。

运行valgrind --tool=callgrind的答案并不完整,没有一些选项。 我们通常不想在Valgrind中简介10分钟的启动时间,并且想要在执行某个任务时对其进行configuration。

所以这是我推荐的。 先运行程序:

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

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

 callgrind_control -i on 

这轮到分析。 要closures它并停止整个任务,我们可以使用:

 callgrind_control -k 

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

 kcachegrind callgrind.out.* 

我build议在下一个窗口点击“Self”列标题,否则显示“main()”是最耗时的任务。 “自我”表明每个function本身需要花费多less时间,而不是与家属一起。

使用Valgrind,callgrind和kcachegrind:

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

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

使用gprof(加上-pg):

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

(不太适合multithreading,函数指针)

使用谷歌perftools:

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

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

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

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

对于CPU绑定应用程序:

  1. 在DEBUG模式下使用一个分析器来识别代码中有问题的部分
  2. 然后切换到RELEASE模式,并注释掉代码中可疑的部分(不加任何东西),直到看到性能的变化。

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

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

NB

如果你没有一个分析器,使用这个可怜的人的分析器。 在debugging应用程序的时候暂停点击。 大多数开发人员套件都会打开带有注释行号的程序集。 你在统计上可能会降落在吃大部分CPU周期的地区。

对于CPU来说,在DEBUG模式下进行性能分析的原因是因为如果你试图在RELEASE模式下进行性能分析,那么编译器会减lessmath,vector化循环和内联函数,这些函数在汇编的时候会把你的代码变成不可映射的混乱。 一个不可映射的混乱意味着你的分析器将不能清楚地识别什么花了这么久,因为程序集可能不符合优化下的源代码 。 如果您需要RELEASE模式的性能(例如时序敏感),请根据需要禁用debugging器function以保持可用的性能。

对于I / O绑定,分析器仍然可以在RELEASE模式下识别I / O操作,因为I / O操作要么在外部链接到共享库(大部分时间),要么在最坏的情况下会导致sys-调用中断向量(这也很容易被profiler识别)。