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

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

658887 次浏览

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

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

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

我还没试过,但我听说过关于谷歌工具的好东西。绝对值得一试。

相关问题这里

如果gprof不适合你,还有一些其他的流行语:Valgrind,IntelVTune,SunDTrace

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

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

停止几次,每次查看调用堆栈。如果有一些代码浪费了一定比例的时间,20%或50%或其他什么,这是你在每个样本上的行为中发现它的概率。所以,这大致是你会看到它的样本的百分比。不需要有根据的猜测。如果你确实对问题是什么有猜测,这将证明或反驳它。

您可能有多个不同大小的性能问题。如果您清除其中任何一个,其余的问题将在后续通过中占据更大的百分比,并且更容易发现。当多个问题复合时,这个放大效应会导致真正巨大的加速因子。

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

  1. 他们不总结在教学水平,和
  2. 他们在递归的存在下给出令人困惑的摘要。

他们还会说它只适用于玩具程序,而实际上它适用于任何程序,而且它似乎在更大的程序上工作得更好,因为他们往往会发现更多的问题。他们会说它有时会发现不是问题的东西,但只有当你看到一些一次时才是正确的。如果你在多个样本上看到问题,那就是真实的。

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的新估计,如下所示:

PriorP(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.259740260.1    0.9   0.81          0.081        0.181          0.470129870.1    0.8   0.64          0.064        0.245          0.6363636360.1    0.7   0.49          0.049        0.294          0.7636363640.1    0.6   0.36          0.036        0.33           0.8571428570.1    0.5   0.25          0.025        0.355          0.9220779220.1    0.4   0.16          0.016        0.371          0.9636363640.1    0.3   0.09          0.009        0.38           0.9870129870.1    0.2   0.04          0.004        0.384          0.9974025970.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很便宜。然后我们得到:

PriorP(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.0727272730.001  0.9  0.81           0.00081      0.00181        0.1316363640.001  0.8  0.64           0.00064      0.00245        0.1781818180.001  0.7  0.49           0.00049      0.00294        0.2138181820.001  0.6  0.36           0.00036      0.0033         0.240.001  0.5  0.25           0.00025      0.00355        0.2581818180.001  0.4  0.16           0.00016      0.00371        0.2698181820.001  0.3  0.09           0.00009      0.0038         0.2763636360.001  0.2  0.04           0.00004      0.00384        0.2792727270.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分布,平均值为0。

(关键是我们不止一次看到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工具读取此文件。它将为您提供对事物的图形分析,并给出结果,例如哪些行花费多少钱。

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

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

Callgrind是一个基于此的分析器。主要好处是您不必运行数小时的应用程序即可获得可靠的结果。即使是一秒钟的运行也足以获得坚如磐石的可靠结果,因为Callgrind是一个非探测分析器。

另一个基于Valgrind构建的工具是Massif。我用它来分析堆内存使用情况。它工作得很好。它的作用是给你内存使用情况的快照——详细信息什么占内存的百分比,谁把它放在那里。这些信息在应用程序运行的不同时间点可用。

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

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

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

替换文本

这是对Nazgob的回答的回应。

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

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

  2. 函数指针混淆了调用图。示例:我有一个名为multithread()的函数,它使我能够在指定数组上多线程指定函数(都作为参数传递)。然而,Gprof将所有对multithread()的调用视为等效的,以计算在儿童身上花费的时间。由于我传递给multithread()的一些函数比其他函数花费的时间长得多,我的调用图大多毫无用处。(对于那些想知道线程化是否是这里的问题的人:不,multithread()可以选择,在这种情况下,仅在调用线程上顺序运行所有内容)。

  3. 这里写着"…调用次数的数字是通过计数而不是采样得出的。它们是完全准确的…"。然而,我发现我的调用图给了我5345859132+784984078作为我最常调用的函数的调用统计数据,其中第一个数字应该是直接调用,第二个递归调用(都来自本身)。由于这意味着我有一个bug,我在代码中放了长(64位)计数器,并再次进行同样的运行。我的计数:5345859132直接调用,78094395406自递归调用。那里有很多数字,所以我要指出我测量的递归调用是780亿,而Gprof的784m:相差100倍。两次运行都是单线程和未经优化的代码,一次编译-g,另一次编译-pg

这是GNUGprof(GNU Binutils for Debian)2.18.0.20080103在64位Debian Lenny下运行,如果这对任何人都有帮助的话。

如果没有一些选项,运行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()”是最耗时的任务。“self”显示每个函数本身花费了多少时间,而不是与依赖项一起。

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

对于CPU受限的应用程序:

  1. 在DEBUG模式下使用分析器来识别代码中有问题的部分
  2. 然后切换到RELEASE模式并注释掉代码中有问题的部分(不使用存根),直到您看到性能的变化。

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

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

注:

如果您没有分析器,请使用穷人的分析器。调试应用程序时点击暂停。大多数开发人员套件将使用注释行号进入汇编。从统计上讲,您很可能会进入一个消耗大部分CPU周期的区域。

对于CPU来说,在调试模式下进行分析的原因是,如果你尝试在释放模式下进行分析,编译器将减少数学、向量化循环和内联函数,这些函数在组装时往往会将你的代码弄得一团糟。无法映射的混乱意味着您的分析器将无法清楚地识别需要这么长时间的原因,因为程序集可能与优化下的源代码不对应.如果你需要释放模式的性能(例如时序敏感),请根据需要禁用调试器功能以保持可用的性能。

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

使用Valgrind、Call grind和kcachegrind:

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

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

使用gprof(add-pg):

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

(不太适合多线程,函数指针)

使用google-的工具:

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

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

其他: AMD Codeanalyer(已被AMD CodeXL取代)、OProfile、'perf'工具(apt-get install linux-ols)

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

它是一个采样分析器,类似于Mike Dunlavey的… long…答案,它将把结果包装在一个可浏览的调用堆栈树中,用每个函数花费的时间或内存进行注释,无论是累积的还是每个函数。

还值得一提的是

  1. HPCToolkit(http://hpctoolkit.org/)-开源,适用于并行程序,并具有用于以多种方式查看结果的GUI
  2. 英特尔VTune(https://software.intel.com/en-us/vtune)-如果你有英特尔编译器,这是非常好的
  3. TAU(http://www.cs.uoregon.edu/research/tau/home.php

我使用过HPCToolkit和VTune,它们非常有效地找到了帐篷中的长杆,并且不需要重新编译您的代码(除了您必须在CMake中使用-g-O或RelWith DebInfo类型构建才能获得有意义的输出)。

您可以使用iprof库:

https://gitlab.com/Neurochrom/iprof

https://github.com/Neurochrom/iprof

它是跨平台的,允许您不实时测量应用程序的性能。您甚至可以将其与实时图表相结合。声明:我是作者

在工作中,我们有一个非常好的工具可以帮助我们监控我们想要的日程安排。这已经很有用了很多次。

它C++,必须根据您的需要进行定制。不幸的是,我不能共享代码,只能共享概念。您使用一个包含时间戳和事件ID的“大型”volatile缓冲区,您可以在事后或停止日志记录系统后转储该缓冲区(例如将其转储到文件中)。

您检索包含所有数据的所谓大缓冲区,一个小接口解析它并显示带有名称(上/下+值)的事件,就像示波器使用颜色(在.hpp文件中配置)一样。

你可以自定义生成的事件量,以仅关注你想要的。它在调度问题上帮助了我们很多,同时根据每秒记录的事件量消耗了我们想要的CPU量。

您需要3个文件:

toolname.hpp // interfacetoolname.cpp // codetool_events_id.hpp // Events ID

这个概念是在tool_events_id.hpp中定义事件,如下所示:

// EVENT_NAME                         ID      BEGIN_END BG_COLOR NAME#define SOCK_PDU_RECV_D               0x0301  //@D00301 BGEEAAAA # TX_PDU_Recv#define SOCK_PDU_RECV_F               0x0302  //@F00301 BGEEAAAA # TX_PDU_Recv

您还在toolname.hpp中定义了一些函数:

#define LOG_LEVEL_ERROR 0#define LOG_LEVEL_WARN 1// ...
void init(void);void probe(id,payload);// etc

在您的代码中,您可以使用:

toolname<LOG_LEVEL>::log(EVENT_NAME,VALUE);

probe函数使用一些装配线来尽快检索时钟时间戳,然后在缓冲区中设置一个条目。我们还有一个原子增量来安全地找到存储日志事件的索引。当然缓冲区是循环的。

希望这个想法不会因为缺少示例代码而混淆。

您可以使用像#0这样的日志框架,因为它包含时间戳和总正常运行时间,可以很好地用于分析:

由于没有人提到Arm MAP,我个人已经成功地使用Map来描述一个C++的科学计划。

Arm MAP是用于并行、多线程或单线程C、C++、Fortran和F90代码的分析器。它提供对源代码行的深入分析和瓶颈定位。与大多数分析器不同,它旨在能够分析并行和线程代码的pthread、OpenMP或MPI。

MAP是商业软件。

实际上有点惊讶没有很多人提到谷歌/基准测试,而固定特定的代码区域有点麻烦,特别是如果代码库有点大,但是我发现这在与callgrind结合使用时非常有帮助

恕我直言,识别导致瓶颈的部分是这里的关键。然而,我会首先尝试回答以下问题,并在此基础上选择工具

  1. 我的算法正确吗?
  2. 有没有被证明是瓶颈的锁?
  3. 是否有特定的代码部分被证明是罪魁祸首?
  4. IO如何处理和优化?

valgrindcallgrindkcachegrind的组合应该对上述几点提供一个不错的估计,一旦确定某些代码部分存在问题,我建议做一个微型基准测试-google benchmark是一个很好的起点。

编译和链接代码并运行可执行文件时使用-pg标志。执行此程序时,分析数据会收集在文件中a.out.
有两种不同类型的分析

1-平面分析:
通过运行命令gprog --flat-profile a.out,您得到以下数据
-函数花费的总时间的百分比,
-在函数中花费了多少秒-包括和排除对子函数的调用,
-调用次数,
-每次通话的平均时间。

2-图形分析
我们使用命令gprof --graph a.out获取每个函数的以下数据,其中包括
-在每个部分中,一个函数用索引号标记。
-在函数之上,有一个调用函数的函数列表。
-在函数下面,有一个函数调用的函数列表。

要获取更多信息,您可以查看https://sourceware.org/binutils/docs-2.32/gprof/

C++分析技术概览:gprof vs valgrind vs perf vs g的工具

在这个答案中,我将使用几种不同的工具来分析几个非常简单的测试程序,以便具体比较这些工具的工作原理。

以下测试程序非常简单,执行以下操作:

  • main调用fastmaybe_slow 3次,其中一个maybe_slow调用很慢

    maybe_slow的慢速调用长10倍,如果我们考虑对子函数common的调用,它将主导运行时。理想情况下,分析工具将能够将我们指向特定的慢速调用。

  • fastmaybe_slow都调用common,这占了程序执行的大部分

  • 程序界面是:

    ./main.out [n [seed]]

    seed只是为了在不影响运行时的情况下获得不同的输出。

main. c

#include <inttypes.h>#include <stdio.h>#include <stdlib.h>
uint64_t __attribute__ ((noinline)) common(uint64_t n, uint64_t seed) {for (uint64_t i = 0; i < n; ++i) {seed = (seed * seed) - (3 * seed) + 1;}return seed;}
uint64_t __attribute__ ((noinline)) fast(uint64_t n, uint64_t seed) {uint64_t max = (n / 10) + 1;for (uint64_t i = 0; i < max; ++i) {seed = common(n, (seed * seed) - (3 * seed) + 1);}return seed;}
uint64_t __attribute__ ((noinline)) maybe_slow(uint64_t n, uint64_t seed, int is_slow) {uint64_t max = n;if (is_slow) {max *= 10;}for (uint64_t i = 0; i < max; ++i) {seed = common(n, (seed * seed) - (3 * seed) + 1);}return seed;}
int main(int argc, char **argv) {uint64_t n, seed;if (argc > 1) {n = strtoll(argv[1], NULL, 0);} else {n = 1;}if (argc > 2) {seed = strtoll(argv[2], NULL, 0);} else {seed = 0;}seed += maybe_slow(n, seed, 0);seed += fast(n, seed);seed += maybe_slow(n, seed, 1);seed += fast(n, seed);seed += maybe_slow(n, seed, 0);seed += fast(n, seed);printf("%" PRIX64 "\n", seed);return EXIT_SUCCESS;}

gprof

gprof要求用检测重新编译软件,它还使用采样方法和检测。因此,它在准确性(采样并不总是完全准确的,可以跳过函数)和执行速度减慢(检测和采样是相对较快的技术,不会大大减慢执行速度)之间取得平衡。

gprof内置于GCC/binutils中,因此我们所要做的就是使用-pg选项编译以启用gprof。然后,我们使用size CLI参数正常运行程序,该参数会产生几秒钟的合理持续时间(10000):

gcc -pg -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.ctime ./main.out 10000

出于教育原因,我们还将在没有启用优化的情况下运行。请注意,这在实践中是无用的,因为你通常只关心优化程序的性能:

gcc -pg -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c./main.out 10000

首先,time告诉我们,使用和不使用-pg的执行时间是相同的,这很棒:没有减速!然而,我看到了复杂软件2-3倍减速的帐户,例如这张票上显示

因为我们使用-pg编译,所以运行程序会生成一个包含分析数据的文件gmon.out文件。

我们可以用gprof2dot以图形方式观察该文件,如以下所示:是否有可能获得gprof结果的图形表示?

sudo apt install graphvizpython3 -m pip install --user gprof2dotgprof main.out > main.gprofgprof2dot < main.gprof | dot -Tsvg -o output.svg

在这里,gprof工具读取gmon.out跟踪信息,并在main.gprof中生成人类可读的报告,然后gprof2dot读取该报告以生成图形。

GProph2Dot的源代码是:https://github.com/jrfonseca/gprof2dot

对于-O0运行,我们观察到以下内容:

在此处输入图片描述

对于-O3运行:

在此处输入图片描述

-O0的输出非常不言自明。例如,它显示3个maybe_slow调用及其子调用占用了总运行时间的97.56%,尽管maybe_slow本身没有子函数的执行占了总执行时间的0.00%,即几乎所有花在该函数上的时间都花在了子函数调用上。

待办事项:为什么-O3输出中缺少main,即使我可以在GDB中的bt上看到它?GProf输出中缺少函数我认为这是因为gprof除了编译仪器外还基于采样,-O3main太快了,没有样本。

我选择SVG输出而不是PNG,因为SVG可以用Ctrl+F搜索,文件大小可以小10倍左右。此外,生成的图像的宽度和高度对于复杂的软件来说可能是巨大的,有数万个像素,GNOMEeog 3.28.1在这种情况下为PNG出错,而SVG由我的浏览器自动打开。

但即使这样,您也会拖动图像以找到您想要的内容,例如,从这张票中的“真实”软件示例中查看此图像:

在此处输入图片描述

你能轻松找到最关键的调用堆栈吗?所有这些微小的未排序的意大利面条行都在一个接一个地堆叠?我确定可能有更好的dot选项,但我现在不想去那里。我们真正需要的是一个合适的专用查看器,但我还没有找到:

但是,您可以使用颜色映射来缓解这些问题。例如,在前面的巨型图像中,当我做出出色的推论时,我终于设法找到了左侧的关键路径,即绿色在红色之后,最后是深色和深蓝色。

或者,我们还可以观察gprof内置binutils工具的文本输出,我们之前保存在:

cat main.gprof

默认情况下,这会产生一个非常冗长的输出,解释输出数据的含义。由于我无法解释得更好,我让你自己阅读。

一旦您了解了数据输出格式,您可以减少冗长以仅显示数据,而无需使用-b选项的教程:

gprof -b main.out

在我们的示例中,输出为-O0

Flat profile:
Each sample counts as 0.01 seconds.%   cumulative   self              self     totaltime   seconds   seconds    calls   s/call   s/call  name100.35      3.67     3.67   123003     0.00     0.00  common0.00      3.67     0.00        3     0.00     0.03  fast0.00      3.67     0.00        3     0.00     1.19  maybe_slow
Call graph

granularity: each sample hit covers 2 byte(s) for 0.27% of 3.67 seconds
index % time    self  children    called     name0.09    0.00    3003/123003      fast [4]3.58    0.00  120000/123003      maybe_slow [3][1]    100.0    3.67    0.00  123003         common [1]-----------------------------------------------<spontaneous>[2]    100.0    0.00    3.67                 main [2]0.00    3.58       3/3           maybe_slow [3]0.00    0.09       3/3           fast [4]-----------------------------------------------0.00    3.58       3/3           main [2][3]     97.6    0.00    3.58       3         maybe_slow [3]3.58    0.00  120000/123003      common [1]-----------------------------------------------0.00    0.09       3/3           main [2][4]      2.4    0.00    0.09       3         fast [4]0.09    0.00    3003/123003      common [1]-----------------------------------------------
Index by function name
[1] common                  [4] fast                    [3] maybe_slow

对于-O3

Flat profile:
Each sample counts as 0.01 seconds.%   cumulative   self              self     totaltime   seconds   seconds    calls  us/call  us/call  name100.52      1.84     1.84   123003    14.96    14.96  common
Call graph

granularity: each sample hit covers 2 byte(s) for 0.54% of 1.84 seconds
index % time    self  children    called     name0.04    0.00    3003/123003      fast [3]1.79    0.00  120000/123003      maybe_slow [2][1]    100.0    1.84    0.00  123003         common [1]-----------------------------------------------<spontaneous>[2]     97.6    0.00    1.79                 maybe_slow [2]1.79    0.00  120000/123003      common [1]-----------------------------------------------<spontaneous>[3]      2.4    0.00    0.04                 fast [3]0.04    0.00    3003/123003      common [1]-----------------------------------------------
Index by function name
[1] common

作为每个部分的快速摘要,例如:

                0.00    3.58       3/3           main [2][3]     97.6    0.00    3.58       3         maybe_slow [3]3.58    0.00  120000/123003      common [1]

以左缩进的函数为中心(maybe_flow)。[3]是该函数的ID。函数上方是它的调用者,下方是调用者。

对于-O3,请参阅此处,就像在图形输出中一样,maybe_slowfast没有已知的父级,这就是留档所说的<spontaneous>的含义。

我不确定是否有一个很好的方法来使用gprof进行逐行分析:“gprof”花费在特定代码行上的时间

valgrind Callgrind

valgrind通过valgrind虚拟机运行程序。这使得分析非常准确,但它也会产生非常大的程序减速。我之前在:获取代码的图形函数调用图的工具中也提到过kcachegrind

Callgrind是valgrind的分析代码的工具,kcachegrind是一个可以可视化cachegrind输出的KDE程序。

首先,我们必须删除-pg标志才能返回正常编译,否则运行实际上会失败#1,是的,这很常见,我做了,并且有一个Stack Overflow问题。

所以我们编译并运行为:

sudo apt install kcachegrind valgrindgcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.ctime valgrind --tool=callgrind valgrind --dump-instr=yes \--collect-jumps=yes ./main.out 10000

我启用了--dump-instr=yes --collect-jumps=yes,因为这也转储了使我们能够以相对较小的额外开销成本查看每个装配线性能细分的信息。

归根结底,time告诉我们程序执行需要29.5秒,所以在这个例子上,我们的速度放慢了大约15倍。显然,这种放慢对于更大的工作负载将是一个严重的限制。在“真实世界的软件示例”这里提到上,我观察到速度放慢了80倍。

运行生成一个名为callgrind.out.<pid>的配置文件数据文件,例如在我的情况下为callgrind.out.8554。我们使用以下方式查看该文件:

kcachegrind callgrind.out.8554

它显示了一个GUI,其中包含类似于文本gprof输出的数据:

在此处输入图片描述

此外,如果我们转到右下角的“调用图”选项卡,我们会看到一个调用图,我们可以通过右键单击它来导出它,以获得以下具有不合理数量的白色边框的图像:-)

在此处输入图片描述

我认为fast没有显示在该图上,因为kcachegrind一定简化了可视化,因为该调用占用的时间太少,这可能是您在实际程序上想要的行为。右键单击菜单有一些设置来控制何时剔除此类节点,但我无法在快速尝试后让它显示如此短的调用。如果我单击左侧窗口中的fast,它确实显示了fast的调用图,因此实际上捕获了堆栈。还没有人找到显示完整图调用图的方法:使Callgrind显示kcachegrind调用图中的所有函数调用

在复杂C++软件的待办事项中,我看到一些类型为<cycle N>的条目,例如<cycle 11>,我希望函数名称,这是什么意思?我注意到有一个“循环检测”按钮可以打开和关闭,但这是什么意思?

perflinux-tools

perf似乎只使用Linux内核采样机制。这使得设置非常简单,但也不完全准确。

sudo apt install linux-toolstime perf record -g ./main.out 10000

这增加了0.2秒的执行时间,所以我们在时间上很好,但在使用键盘右箭头扩展common节点后,我仍然没有看到太多兴趣:

Samples: 7K of event 'cycles:uppp', Event count (approx.): 6228527608Children      Self  Command   Shared Object     Symbol-   99.98%    99.88%  main.out  main.out          [.] commoncommon0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e70.01%     0.01%  main.out  [kernel]          [k] 0xffffffff8a6001580.01%     0.00%  main.out  [unknown]         [k] 0x00000000000000400.01%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start0.01%     0.00%  main.out  ld-2.27.so        [.] dl_main0.01%     0.00%  main.out  ld-2.27.so        [.] mprotect0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object0.01%     0.00%  main.out  ld-2.27.so        [.] _xstat0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init0.00%     0.00%  main.out  [unknown]         [.] 0x2f3d4f49445554530.00%     0.00%  main.out  [unknown]         [.] 0x00007fff3cfc57ac0.00%     0.00%  main.out  ld-2.27.so        [.] _start

然后我尝试对-O0程序进行基准测试,看看它是否显示了什么,直到现在,我终于看到了一个调用图:

Samples: 15K of event 'cycles:uppp', Event count (approx.): 12438962281Children      Self  Command   Shared Object     Symbol+   99.99%     0.00%  main.out  [unknown]         [.] 0x04be258d4c544155+   99.99%     0.00%  main.out  libc-2.27.so      [.] __libc_start_main-   99.99%     0.00%  main.out  main.out          [.] main- main- 97.54% maybe_slowcommon- 2.45% fastcommon+   99.96%    99.85%  main.out  main.out          [.] common+   97.54%     0.03%  main.out  main.out          [.] maybe_slow+    2.45%     0.00%  main.out  main.out          [.] fast0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e70.00%     0.00%  main.out  [unknown]         [k] 0x00000000000000400.00%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start0.00%     0.00%  main.out  ld-2.27.so        [.] dl_main0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_lookup_symbol_x0.00%     0.00%  main.out  [kernel]          [k] 0xffffffff8a6001580.00%     0.00%  main.out  ld-2.27.so        [.] mmap640.00%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init0.00%     0.00%  main.out  [unknown]         [.] 0x552e53555f6e653d0.00%     0.00%  main.out  [unknown]         [.] 0x00007ffe1cf20fdb0.00%     0.00%  main.out  ld-2.27.so        [.] _start

待办事项:-O3执行时发生了什么?只是maybe_slowfast太快而没有得到任何样本吗?在执行时间更长的大型程序上,它与-O3一起工作吗?我是否错过了一些CLI选项?我发现了大约-F来控制赫兹的样本频率,但我把它调高到-F 39500默认允许的最大值(可以用sudo增加),我仍然没有看到明确的调用。

关于perf的一个很酷的事情是Brendan Gregg的FlameGraph工具,它以一种非常整洁的方式显示调用堆栈计时,让您可以快速查看大调用。该工具可在:https://github.com/brendangregg/FlameGraph获得,也在他的perf教程中提到:http://www.brendangregg.com/perf.html#FlameGraphs当我在没有sudo的情况下运行perf时,我得到了#3,所以现在我将使用sudo来做它:

git clone https://github.com/brendangregg/FlameGraphsudo perf record -F 99 -g -o perf_with_stack.data ./main.out 10000sudo perf script -i perf_with_stack.data | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flamegraph.svg

但是在这样一个简单的程序中,输出不是很容易理解,因为我们在该图上既看不到maybe_slow也看不到fast

在此处输入图片描述

在一个更复杂的例子中,图表的含义变得很清楚:

在此处输入图片描述

待办事项在这个例子中有一个[unknown]函数的日志,这是为什么?

另一个可能值得的perf GUI界面包括:

  • Eclipse Trace Compass插件:https://www.eclipse.org/tracecompass/

    但这有一个缺点,你必须首先将数据转换为通用跟踪格式,这可以用perf data --to-ctf完成,但它需要在构建时启用/有perf足够新的,其中任何一个都不是Ubuntu 18.04中perf的情况

  • https://github.com/KDAB/hotspot

    这样做的缺点是似乎没有Ubuntu包,构建它需要Qt 5.10,而Ubuntu 18.04需要Qt 5.9。

    但是davidfaure在评论中提到没有AppImage包,这可能是使用它的便捷方式。

g的工具

以前称为“Google性能工具”,来源:https://github.com/gperftools/gperftools Sample based。

首先使用以下命令安装g的工具:

sudo apt install google-perftools

然后,我们可以通过两种方式启用g的CPU分析器:在运行时或在构建时。

在运行时,我们必须通过设置LD_PRELOAD指向libprofiler.so,您可以在locate libprofiler.so中找到它,例如在我的系统上:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.cLD_PRELOAD=/usr/lib/x86_64-linux-gnu/libprofiler.so \CPUPROFILE=prof.out ./main.out 10000

或者,我们可以在链接时构建库,在运行时分配传递LD_PRELOAD

gcc -Wl,--no-as-needed,-lprofiler,--as-needed -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.cCPUPROFILE=prof.out ./main.out 10000

另见:g的工具-配置文件不转储

到目前为止,我发现查看这些数据的最好方法是使pprof输出与kcachegrind作为输入的格式相同(是的,Valgrind-project ect-vier-ool),并使用kcachegrind查看:

google-pprof --callgrind main.out prof.out  > callgrind.outkcachegrind callgrind.out

使用这两种方法中的任何一种运行后,我们会得到一个prof.out配置文件数据文件作为输出。我们可以使用以下命令以图形方式将该文件视为SVG:

google-pprof --web main.out prof.out

在此处输入图片描述

它像其他工具一样给出熟悉的调用图,但使用笨重的样本数单位而不是秒。

或者,我们也可以通过以下方式获取一些文本数据:

google-pprof --text main.out prof.out

其中规定:

Using local file main.out.Using local file prof.out.Total: 187 samples187 100.0% 100.0%      187 100.0% common0   0.0% 100.0%      187 100.0% __libc_start_main0   0.0% 100.0%      187 100.0% _start0   0.0% 100.0%        4   2.1% fast0   0.0% 100.0%      187 100.0% main0   0.0% 100.0%      183  97.9% maybe_slow

另见:如何使用google perf工具

使用原始perf_event_open系统调用检测您的代码

我认为这与perf使用的底层子系统相同,但是您当然可以通过在编译时显式地检测您的程序以及感兴趣的事件来获得更大的控制。

这对大多数人来说可能太硬核了,但它很有趣。最小可运行示例:计算C程序中执行的指令数量的快速方法

英特尔VTune

https://en.wikipedia.org/wiki/VTune

这似乎是闭源且仅限x86,但据我所知,它可能令人惊叹。我不确定它的使用自由程度,但它似乎可以免费下载。待办事项评估。

测试在Ubuntu 18.04,GProph2Dot 2019.11.30,valgrind 3.13.0,perf 4.15.18,Linux内核4.15.0,FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b,GFO工具2.5-2。

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

只是认为你有一个障碍而你是在运动然后它会降低你的速度

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

g++ your_prg.cpp -pgcc my_program.cpp -g -pg根据您的编译器

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

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

它将生成一个名为gmon.out或callgrind.out.x的文件。然后,您可以使用kcachegrind或调试器工具来读取此文件。它将为您提供一个图形分析,并给出诸如哪些行花费多少钱之类的结果。

我想是的