如何描述 Linux 上运行的 C ++ 代码?

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

答案

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

但是,如果您急于在主观上很慢的情况下在调试器下手动中断程序,则有一种简单的方法可以查找性能问题。

暂停几次,每次查看调用堆栈。如果有一些代码浪费了一定比例的时间(20%或 50%或其他),那么这就是您在每次采样时都将其捕获的概率。因此,这大约是您将看到样品的百分比。不需要有根据的猜测。如果您确实怀疑问题出在哪里,这将证明或不证明它。

您可能会遇到多个不同大小的性能问题。如果您清除其中任何一个,其余的将在以后的传递中占更大的比例,并且更容易发现。当放大多个问题时,这种放大效果会导致真正巨大的加速因素。

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

  1. 他们没有在教学水平上进行总结,并且
  2. 在递归存在的情况下,它们给出了令人困惑的摘要。

他们还会说,它实际上仅对玩具程序有效,而实际上对任何程序都有效,并且似乎在较大的程序上效果更好,因为它们往往会发现更多的问题。他们会说有时发现没有问题的东西,但这只有在您看到一次之后才是真的。如果您在多个样本上发现问题,那是真实的。

PS 如果可以像 Java 中那样在某个时间点收集线程池的调用堆栈样本,也可以在多线程程序上完成。

PPS 大致来说,软件中的抽象层越多,您越有可能发现这是性能问题的原因(并且有提高速度的机会)。

补充:可能并不明显,但是在存在递归的情况下,堆栈采样技术同样有效。原因是通过删除一条指令可以节省的时间大约等于包含该指令的样本所占的比例,而不管该指令在一个样本中可能发生的次数。

我经常听到的另一个反对意见是:“ 它将在某个地方随机停止,并且将错过真正的问题 ”。这源于对实际问题有一个先验的概念。性能问题的一个关键特性是它们无法兑现预期。抽样告诉您某些问题,而您的第一个反应是难以置信。那是很自然的,但是您可以确定它是否发现了真正的问题,反之亦然。

添加:让我对它的工作方式进行贝叶斯解释。假设有一条指令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)。换句话说,我们的先验是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 | 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为成本,那么I将出现在nf+/-sqrt(nf(1-f))样本上。 , n=10f=0.3 ,即3+/-1.4样本。)


添加,以便直观地了解测量和随机堆栈采样之间的区别:
现在有分析器可以对堆栈进行采样,即使是在墙上时钟的时间,但结果就是测量值(或热路径或热点,“瓶颈” 可以从中轻松隐藏)。他们没有向您显示(并且很容易做到)是实际样本本身。而且,如果您的目标是找到瓶颈,那么平均而言 ,您需要查看的瓶颈数量为 2 除以所需的时间。因此,如果花费 30%的时间,平均将显示 2 / .3 = 6.7 个样本,而 20 个样本将显示 99.2%的机会。

这是检验测量值和检验烟囱样品之间差异的现成插图。瓶颈可能是这样的一个大斑点,也可能是很多小的斑点,这没有什么区别。

在此处输入图片说明

测量是水平的;它告诉您特定例程花费的时间比例。采样是垂直的。如果有什么办法可以避免整个程序在那时的工作, 并且在第二个示例中看到它 ,那么您已经找到了瓶颈。这就是与众不同的原因 - 查看花费时间的全部原因,而不仅仅是花多少时间。

您可以将Valgrind与以下选项一起使用

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

它将生成一个名为callgrind.out.x的文件。然后,您可以使用kcachegrind工具读取此文件。它将为您提供图形化的事物分析结果,例如哪些行花费多少。

我认为您正在使用 GCC。标准解决方案是使用gprof 进行分析

在进行概要分析之前,请确保将-pg添加到编译中:

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

我还没有尝试过,但是我听说过有关google-perftools 的消息 。绝对值得一试。

相关问题在这里

如果gprof不能为您完成任务,那么还有一些其他流行语: Valgrind ,Intel VTune ,Sun DTrace

较新的内核(例如最新的 Ubuntu 内核)附带了新的 “perf” 工具(即apt-get install linux-tools ),也称为 perf_events

这些附带经典的采样分析器( 手册页 )以及超赞的时间表

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

替代文字

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

(维基百科)Valgrind 本质上是使用实时(JIT)编译技术(包括动态重新编译)的虚拟机。原始程序中的任何内容都无法直接在主机处理器上运行。相反,Valgrind 首先将程序转换为称为中间表示(IR)的临时,简单形式,该形式是与处理器无关的,基于 SSA 的形式。转换之后,在 Valgrind 将 IR 转换回机器代码并让主处理器运行它之前,可以使用一种工具(见下文)在 IR 上进行任何所需的转换。

Callgrind 是基于此的探查器。主要好处是您不必花费数小时即可完成可靠的结果。因为 Callgrind 是非探测轮廓仪,所以即使是一秒钟的运行也足以获得坚如磐石的可靠结果。

基于 Valgrind 的另一个工具是 Massif。我用它来分析堆内存使用情况。效果很好。它的作用是为您提供内存使用情况的快照 - 详细信息什么占内存的百分比,并且 WHO 已将其放置在那里。此类信息在应用程序运行的不同时间点可用。

没有一些选项,运行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()” 是最耗时的任务。 “自我” 显示了每个功能本身花费的时间,而不是依赖关系。

这是对Nazgob 的 Gprof 回答的回应

最近几天我一直在使用 Gprof,并且已经发现了三个重要的局限性,其中一个我还没有看到其他地方的文档(至今):

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

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

  3. 在这里说:“... 呼叫次数数字是通过计数而不是抽样得出的。它们是完全准确的...”。但是我发现我的调用图给了我 5345859132 + 784984078 作为我最被调用函数的调用统计信息,其中第一个数字应该是直接调用,而第二个递归调用(全部来自其本身)。由于这暗示我有一个错误,因此我在代码中放入了长(64 位)计数器,然后再次执行相同的操作。我的计数是:5345859132 直接和 78094395406 自递归调用。那里有很多数字,所以我要指出,我测量的递归调用为 780 亿,而 Gprof 则为 7.84 亿:相差 100 倍。两次运行都是单线程且未经优化的代码,一个运行-g ,另一个运行-pg

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

使用 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(已由 AMD CodeXL 取代),OProfile,“性能” 工具(apt-get install linux-tools)

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

它是一个采样探查器,遵循... long ... 的答案,由 Mike Dunlavey 回答,它将把结果包装在可浏览的调用堆栈树中,并在每个函数上花费的时间或内存进行注释,无论是累积的还是每个功能。

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

对于受 CPU 约束的应用程序:

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

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

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

NB

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

对于 CPU,在DEBUG模式下进行性能分析的原因是,如果在RELEASE模式下尝试进行性能分析,则编译器将减少数学运算,向量化循环和内联函数,这在汇编代码时往往会使您的代码陷入无法映射的混乱状态。 不可映射的混乱意味着您的探查器将无法清楚地识别花费了很长时间的内容,因为程序集可能与优化后的源代码不符 。如果您需要RELEASE模式的性能(例如,时序敏感),请根据需要禁用调试器功能以保持可用的性能。

对于受 I / O 限制的事件,探查器仍可以在RELEASE模式下识别 I / O 操作,因为 I / O 操作(大多数情况下)是外部链接到共享库的,或者在最坏的情况下,将导致系统崩溃。调用中断向量(事件探查器也可以轻松识别)。