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

Posted

技术标签:

【中文标题】如何分析在 Linux 上运行的 C++ 代码?【英文标题】:How can I profile C++ code running on Linux? 【发布时间】:2010-09-27 09:59:24 【问题描述】:

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

【问题讨论】:

如果您提供更多关于您的开发堆栈的数据,您可能会得到更好的答案。有来自 Intel 和 Sun 的分析器,但您必须使用他们的编译器。这是一个选择吗? 已经在以下链接回答:***.com/questions/2497211/… 大部分答案都是code profilers。但是,优先级反转、缓存别名、资源争用等都可能是优化和性能的因素。我认为人们将信息读入我的慢代码。常见问题都引用了这个线程。 CppCon 2015: Chandler Carruth "Tuning C++: Benchmarks, and CPUs, and Compilers! Oh My!" 我以前随机使用pstack,大部分时间会打印出程序大部分时间最典型的堆栈,从而指向瓶颈。 【参考方案1】:

我假设您使用的是 GCC。标准解决方案是使用gprof 进行分析。

请务必在分析之前将-pg 添加到编译中:

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

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

相关问题here。

如果gprof 不适合您,请使用其他一些流行语:Valgrind、Intel VTune、Sun DTrace。

【讨论】:

我同意 gprof 是当前的标准。不过请注意,Valgrind 用于分析程序的内存泄漏和其他与内存相关的方面,而不是用于速度优化。 Bill,在 vaglrind 套件中,您可以找到 callgrind 和 massif。两者都对分析应用非常有用 @Bill-the-Lizard:gprof 上的一些 cmets:***.com/questions/1777556/alternatives-to-gprof/… gprof -pg 只是调用堆栈分析的近似值。它插入 mcount 调用以跟踪哪些函数正在调用哪些其他函数。它使用基于标准时间的采样,呃,时间。然后,它将函数 foo() 中采样的时间分配给 foo() 的调用者,与调用次数成比例。所以它不区分不同费用的调用。 使用 clang/clang++,可以考虑使用gperftools 的 CPU 分析器。警告:我自己还没有这样做。【参考方案2】:

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

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

只需暂停几次,每次都查看调用堆栈。如果有一些代码浪费了一定比例的时间,20% 或 50% 或其他任何时间,这就是您在每个样本的行为中捕获它的概率。因此,这大致是您将看到它的样本的百分比。不需要有根据的猜测。如果您确实猜到了问题所在,这将证明或反驳它。

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

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

    它们不会在指令级别进行总结,并且 存在递归时,它们会给出令人困惑的摘要。

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

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

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

另一种看待它的方式称为Rule Of Succession。 如果你掷硬币两次,两次都出现正面,这说明硬币的可能重量是什么? 尊重的回答方式是说它是一个 Beta 分布,平均值为(number of hits + 1) / (number of tries + 2) = (2+1)/(2+2) = 75%

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

因此,即使是极少数的样本也可以告诉我们很多关于它所看到的指令成本的信息。 (平均而言,它会以与其成本成正比的频率看到它们。如果采取n 样本,并且f 是成本,那么I 将出现在nf+/-sqrt(nf(1-f)) 样本上。例如,@987654345 @,f=0.3,即3+/-1.4 样本。)


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

以下是检查测量值和检查堆栈样本之间差异的现成说明。 瓶颈可能是这样的一个大块,也可能是许多小块,没有区别。

测量是水平的;它告诉您特定例程需要多少时间。 采样是垂直的。 如果有任何方法可以避免此时整个程序正在执行的操作,如果您在第二个示例中看到它,那么您已经找到了瓶颈。 这就是区别所在 - 了解花费时间的全部原因,而不仅仅是花费多少时间。

【讨论】:

这基本上是一个穷人的抽样分析器,这很棒,但是你冒着样本量太小的风险,这可能会给你带来完全虚假的结果。 @Crash:我不会争论“穷人”部分 :-) 统计测量精度确实需要很多样本,但是有两个相互冲突的目标——测量和问题定位。我专注于后者,为此您需要位置精度,而不是测量精度。因此,例如,在堆栈中间,可以有一个函数调用 A();这占了 50% 的时间,但它可以在另一个大函数 B 中,以及对 A() 的许多其他不昂贵的调用。函数时间的精确摘要可能是一个线索,但每个其他堆栈样本都会查明问题。 ...全世界似乎都认为带有调用计数和/或平均时间注释的调用图就足够了。它不是。可悲的是,对于那些对调用堆栈进行采样的人,最有用的信息就在他们面前,但他们为了“统计”而将其丢弃。 我并不是不同意你的技术。显然,我非常依赖堆栈遍历采样分析器。我只是指出,现在有一些工具可以自动执行此操作,当您超过将功能从 25% 降至 15% 并需要将其从 1.2% 降至0.6%。 -1:不错的想法,但是如果你在一个以绩效为导向的环境中工作也能获得报酬,这就是浪费每个人的时间。使用真正的分析器,这样我们就不必跟在您身后解决实际问题。【参考方案3】:

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

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

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

【讨论】:

valgrind 很棒,但要注意它会让你的程序变慢 还可以查看Gprof2Dot,了解一种令人惊叹的可视化输出的替代方法。 ./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg @neves 是的 Valgrind 在实时分析“gstreamer”和“opencv”应用程序的速度方面并不是很有帮助。 @Sebastian:gprof2dot 现在在这里:github.com/jrfonseca/gprof2dot 要记住的一件事是编译包含调试符号但进行优化,以获得一些可探索的东西,但速度特征类似于实际的“发布”构建。【参考方案4】:

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

(wikipedia) Valgrind 本质上是一个虚拟的 使用即时 (JIT) 的机器 编译技术,包括 动态重新编译。无从 原始程序曾经运行过 直接在主机处理器上。 相反,Valgrind 首先翻译 编程为一个临时的、更简单的形式 称为中间表示 (IR),与处理器无关, 基于 SSA 的表格。转换后, 一个工具(见下文)是免费的 它想要的任何转换 在 IR 上,在 Valgrind 翻译之前 IR 回到机器代码并让 主处理器运行它。

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

另一个基于 Valgrind 的工具是 Massif。我用它来分析堆内存使用情况。它工作得很好。它的作用是为您提供内存使用情况的快照——详细信息 WHAT 拥有多少内存百分比,以及谁将它放在那里。此类信息在应用程序运行的不同时间点可用。

【讨论】:

【参考方案5】:

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

这些带有经典的采样分析器 (man-page) 以及很棒的 timechart!

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

【讨论】:

很棒的工具!无论如何,我是否可以获得从“main->func1->fun2”样式开始的典型“蝴蝶”视图?我似乎无法弄清楚......perf report 似乎给了我调用父母的函数名称......(所以它有点像倒置的蝴蝶视图) 威尔,可以显示线程活动的时间表;添加了 CPU 编号信息?我想查看每个 CPU 上运行的时间和线程。 @kizzx2 - 你可以使用gprof2dotperf script。非常好的工具! 即使是像 4.13 这样的新内核也有用于分析的 eBPF。见brendangregg.com/blog/2015-05-15/ebpf-one-small-step.html 和brendangregg.com/ebpf.html 这应该是公认的答案。使用调试器会在样本中引入过多的噪音。 linux 的性能计数器适用于多线程、多进程、用户和内核空间,这很棒。您还可以检索许多有用的信息,例如分支和缓存未命中。在@AndrewStern 提到的同一个网站上,有一个对这种分析非常有用的火焰图:flame graphs。它生成 SVG 文件,可以使用 Web 浏览器打开交互式图形!【参考方案6】:

这是对Nazgob's Gprof answer的回复。

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

    它不能在多线程代码上正常工作,除非你使用workaround

    调用图被函数指针弄糊涂了。示例:我有一个名为multithread() 的函数,它使我能够在指定数组上对指定函数进行多线程处理(均作为参数传递)。然而,Gprof 将所有对multithread() 的调用视为计算在儿童身上花费的时间的等效方法。由于我传递给multithread() 的某些函数比其他函数花费的时间要长得多,所以我的调用图大多没用。 (对于那些想知道线程是否是这里的问题的人:不,multithread() 可以选择并且在这种情况下确实只在调用线程上按顺序运行所有内容)。

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

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

【讨论】:

是的,它会进行采样,但不适用于通话次数数据。有趣的是,点击您的链接最终将我带到了我在帖子中链接到的手册页的更新版本,新 URL:sourceware.org/binutils/docs/gprof/… 这重复了我回答的 (iii) 部分中的引用,但也说“在多线程中应用程序或与多线程库链接的单线程应用程序,计数只有在计数函数是线程安全的情况下才具有确定性。(注意:请注意 glibc 中的 mcount 计数函数不是线程安全的)。" 我不清楚这是否解释了我在 (iii) 中的结果。我的代码链接了 -lpthread -lm 并声明了“pthread_t *thr”和“pthread_mutex_t nextLock = PTHREAD_MUTEX_INITIALIZER”静态变量,即使它在单线程运行时也是如此。我通常会假设“与多线程库的链​​接”意味着实际使用这些库,并且比这更大,但我可能错了!【参考方案7】:

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

【讨论】:

现在由于某种原因 callgrind.out.* 文件总是空的。执行 callgrind_control -d 对于强制将数据转储到磁盘很有用。 不能。我通常的上下文是整个 mysqlphp 或类似的大东西。往往一开始甚至不知道我想分开什么。 或者在我的情况下,我的程序实际上将一堆数据加载到 LRU 缓存中,我不想对其进行分析。因此,我在启动时强制加载缓存的一个子集,并仅使用该数据分析代码(让 OS+CPU 管理我的缓存中的内存使用)。它可以工作,但是在我试图在不同上下文中分析的代码中加载该缓存很慢并且 CPU 密集型,因此 callgrind 会产生严重污染的结果。 还有CALLGRIND_TOGGLE_COLLECT以编程方式启用/禁用收集;见***.com/a/13700817/288875 @TõnuSamuel,对我来说 callgrind.out.* 是空的。就我而言,程序在分析时崩溃了。解决崩溃原因后,我可以看到 callgrind.out.* 文件中的内容。【参考方案8】:

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

对于 CPU 密集型应用程序:

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

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

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

注意

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

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

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

【讨论】:

+1 穷人的方法对于 I/O 限制和 CPU 限制同样有效,我建议在 DEBUG 模式下进行所有性能调整。完成调音后,打开 RELEASE。如果程序在您的代码中受 CPU 限制,它将有所改进。 Here's a crude but short video of the process. 我不会使用 DEBUG 构建进行性能分析。我经常看到 DEBUG 模式下的性能关键部分在发布模式下被完全优化掉了。另一个问题是在调试代码中使用断言会增加性能噪音。 你读过我的帖子吗? “如果您需要 RELEASE 模式的性能(例如时序敏感),请根据需要禁用调试器功能以保持可用性能”,“然后切换到 RELEASE 模式并注释代码的有问题的部分(Stub it with nothing)直到你看到性能变化。”?我说在调试模式下检查可能的问题区域并在发布模式下验证这些问题以避免您提到的陷阱。【参考方案9】:

使用 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、“perf”工具(apt-get install linux-tools)

【讨论】:

【参考方案10】:

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

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

【讨论】:

看起来很有趣,但无法使用 GCC 9.2 编译。 (Debian/Sid) 我在 github 上提出了一个问题。【参考方案11】:

另外值得一提的是

    HPCToolkit (http://hpctoolkit.org/) - 开源,适用于并行程序,并有一个 GUI,可以通过多种方式查看结果 Intel VTune (https://software.intel.com/en-us/vtune) - 如果你有 Intel 编译器,那就太好了 TAU (http://www.cs.uoregon.edu/research/tau/home.php)

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

【讨论】:

【参考方案12】:

您可以使用 iprof 库:

https://gitlab.com/Neurochrom/iprof

https://github.com/Neurochrom/iprof

它是跨平台的,因此您无需实时测量应用程序的性能。您甚至可以将它与实时图表结合使用。 完全免责声明:我是作者。

【讨论】:

【参考方案13】:

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

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

您使用所有数据检索所谓的大缓冲区,然后一个小界面对其进行解析并显示带有名称(上/下 + 值)的事件,就像示波器使用颜色(在 .hpp 文件中配置)一样。

您可以自定义生成的事件数量,以专注于您想要的。它对我们调度问题有很大帮助,同时根据每秒记录的事件量消耗我们想要的 CPU 量。

你需要 3 个文件:

toolname.hpp // interface
toolname.cpp // code
tool_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 函数使用几条装配线尽快检索时钟时间戳,然后在缓冲区中设置一个条目。我们还有一个原子增量来安全地找到存储日志事件的索引。 当然缓冲区是循环的。

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

【讨论】:

【参考方案14】:

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

【讨论】:

【参考方案15】:

由于没有人提到 Arm MAP,我个人将其添加为我已成功使用 Map 来分析 C++ 科学程序。

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

MAP 是商业软件。

【讨论】:

【参考方案16】:

其实有点意外google/benchmark 没有多少人提到,虽然固定代码的特定区域有点麻烦,特别是如果代码库有点大,但是我发现这在组合使用时非常有用callgrind

恕我直言,识别导致瓶颈的部分是这里的关键。不过,我会先尝试回答以下问题,然后根据这些问题选择工具

    我的算法正确吗? 是否有锁被证明是瓶颈? 是否有特定的代码部分被证明是罪魁祸首? IO、处理和优化怎么样?

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

【讨论】:

当我测量代码段时,我发现我的 google 基准测试数字看起来比 gprof 更准确。正如你所说,它非常适合微基准测试。但如果您想要更全面的图景,则需要采用不同的方法。【参考方案17】:

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

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

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

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

【讨论】:

【参考方案18】:

C++ 分析技术调查:gprof vs valgrind vs perf vs gperftools

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

下面的测试程序很简单,做了以下事情:

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

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

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

程序界面为:

./main.out [n [seed]]

并且程序总共执行O(n^2) 循环。 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。然后,我们使用大小 CLI 参数正常运行程序,该参数会产生合理持续时间的几秒钟 (10000):

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

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

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

首先,time 告诉我们,有和没有-pg 的执行时间是一样的,这很好:没有减速!然而,我已经看到复杂软件的 2 倍 - 3 倍减速的说法,例如作为shown in this ticket。

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

我们可以使用gprof2dot 以图形方式观察该文件,如Is it possible to get a graphical representation of gprof results? 所要求的那样

sudo apt install graphviz
python3 -m pip install --user gprof2dot
gprof main.out > main.gprof
gprof2dot < main.gprof | dot -Tsvg -o output.svg

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

gprof2dot 的来源是:https://github.com/jrfonseca/gprof2dot

对于-O0 运行,我们观察到以下情况:

对于-O3 运行:

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

TODO:为什么 -O3 输出中缺少 main,即使我可以在 GDB 中的 bt 上看到它? Missing function from GProf output 我认为这是因为 gprof 除了编译后的检测之外,也是基于采样的,而 -O3 main 太快了,没有采样。

我选择 SVG 输出而不是 PNG,因为可以使用 Ctrl + F 搜索 SVG,并且文件大小可以小 10 倍左右。此外,对于复杂的软件,生成的图像的宽度和高度可能会达到数万像素,而 GNOME eog 3.28.1 在这种情况下会出现 PNG 的错误,而 SVG 会被我的浏览器自动打开。 gimp 2.8 运行良好,另请参阅:

https://askubuntu.com/questions/1112641/how-to-view-extremely-large-images https://unix.stackexchange.com/questions/77968/viewing-large-image-on-linux https://superuser.com/questions/356038/viewer-for-huge-images-under-linux-100-mp-color-images

但即便如此,您仍需要将图像拖到很多地方才能找到所需的内容,例如,请参阅此图片来自this ticket 中的“真实”软件示例:

如果所有那些细小的未排序的意大利面条线相互重叠,您能否轻松找到最关键的调用堆栈?我敢肯定,可能会有更好的dot 选项,但我现在不想去那里。我们真正需要的是一个合适的专用查看器,但我还没有找到:

View gprof output in kcachegrind Which is the best replacement for KProf?

但是,您可以使用颜色图来稍微缓解这些问题。比如在之前的巨幅图像上,当我巧妙地演绎出绿色后红色,最后是越来越深的蓝色时,我终于找到了左边的关键路径。

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

cat main.gprof

默认情况下,这会产生一个非常详细的输出,解释输出数据的含义。既然我无法解释得比这更好,那我就让你自己看吧。

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

gprof -b main.out

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

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
100.35      3.67     3.67   123003     0.00     0.00  common
  0.00      3.67     0.00        3     0.00     0.03  fast
  0.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     name
                0.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     total           
 time   seconds   seconds    calls  us/call  us/call  name    
100.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     name
                0.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 没有已知的父级,这就是文档所说的&lt;spontaneous&gt; 的意思。

我不确定是否有使用 gprof 进行逐行分析的好方法:`gprof` time spent in particular lines of code

valgrind 调用研磨

valgrind 通过 valgrind 虚拟机运行程序。这使得分析非常准确,但它也会导致程序非常慢。我之前也提到过 kcachegrind:Tools to get a pictorial function call graph of code

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

首先我们必须删除-pg 标志才能恢复正常编译,否则运行实际上会失败并显示Profiling timer expired,是的,这很常见,以至于我这样做了,并且有一个堆栈溢出问题。

所以我们编译运行如下:

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

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

马上,time 告诉我们程序执行需要 29.5 秒,因此在这个示例中我们的速度降低了大约 15 倍。显然,这种放缓将成为更大工作负载的严重限制。在“真实世界软件示例”mentioned here 中,我观察到速度下降了 80 倍。

运行会生成一个名为 callgrind.out.&lt;pid&gt; 的配置文件数据文件,例如callgrind.out.8554 就我而言。我们通过以下方式查看该文件:

kcachegrind callgrind.out.8554

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

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

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

复杂 C++ 软件上的 TODO,我看到一些 &lt;cycle N&gt; 类型的条目,例如&lt;cycle 11&gt; 我期望函数名称,这是什么意思?我注意到有一个“循环检测”按钮可以打开和关闭它,但这是什么意思?

perf 来自linux-tools

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

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

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

Samples: 7K of event 'cycles:uppp', Event count (approx.): 6228527608     
  Children      Self  Command   Shared Object     Symbol                  
-   99.98%    99.88%  main.out  main.out          [.] common              
     common                                                               
     0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e7  
     0.01%     0.01%  main.out  [kernel]          [k] 0xffffffff8a600158  
     0.01%     0.00%  main.out  [unknown]         [k] 0x0000000000000040  
     0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start    
     0.01%     0.00%  main.out  ld-2.27.so        [.] dl_main             
     0.01%     0.00%  main.out  ld-2.27.so        [.] mprotect            
     0.01%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object      
     0.01%     0.00%  main.out  ld-2.27.so        [.] _xstat              
     0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init
     0.00%     0.00%  main.out  [unknown]         [.] 0x2f3d4f4944555453  
     0.00%     0.00%  main.out  [unknown]         [.] 0x00007fff3cfc57ac  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _start              

然后我尝试对-O0 程序进行基准测试以查看它是否显示任何内容,直到现在,我终于看到了调用图:

Samples: 15K of event 'cycles:uppp', Event count (approx.): 12438962281   
  Children      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_slow                                                 
           common                                                         
      - 2.45% fast                                                        
           common                                                         
+   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          [.] fast                
     0.11%     0.11%  main.out  [kernel]          [k] 0xffffffff8a6009e7  
     0.00%     0.00%  main.out  [unknown]         [k] 0x0000000000000040  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_sysdep_start    
     0.00%     0.00%  main.out  ld-2.27.so        [.] dl_main             
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_lookup_symbol_x 
     0.00%     0.00%  main.out  [kernel]          [k] 0xffffffff8a600158  
     0.00%     0.00%  main.out  ld-2.27.so        [.] mmap64              
     0.00%     0.00%  main.out  ld-2.27.so        [.] _dl_map_object      
     0.00%     0.00%  main.out  ld-2.27.so        [.] __GI___tunables_init
     0.00%     0.00%  main.out  [unknown]         [.] 0x552e53555f6e653d  
     0.00%     0.00%  main.out  [unknown]         [.] 0x00007ffe1cf20fdb  
     0.00%     0.00%  main.out  ld-2.27.so        [.] _start              

TODO:-O3 执行时发生了什么?难道仅仅是maybe_slowfast 太快了,没有得到任何样本吗? -O3 在需要更长执行时间的大型程序上是否能正常工作?我错过了一些 CLI 选项吗?我发现-F 可以控制以赫兹为单位的采样频率,但我将其调高到默认允许的最大值-F 39500(可以使用sudo 增加),我仍然看不到清晰的调用。

perf 的一个很酷的地方是 Brendan Gregg 的 FlameGraph 工具,它以非常简洁的方式显示调用堆栈时间,让您可以快速查看大调用。该工具位于:https://github.com/brendangregg/FlameGraph,并且在他的性能教程中也提到:http://www.brendangregg.com/perf.html#FlameGraphs 当我在没有sudo 的情况下运行perf 时,我得到了ERROR: No stack counts found,所以现在我将使用sudo

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

但是在这样一个简单的程序中,输出并不是很容易理解,因为我们不能轻易地在该图上看到maybe_slowfast

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

TODO 在那个例子中有一个[unknown] 函数的日志,这是为什么呢?

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

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

但这有一个缺点,您必须首先将数据转换为通用跟踪格式,这可以使用perf data --to-ctf 完成,但需要在构建时启用/具有足够新的perf,无论是Ubuntu 18.04 中的性能并非如此

https://github.com/KDAB/hotspot

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

gperftools

以前称为“Google 性能工具”,来源:https://github.com/gperftools/gperftools 基于示例。

首先安装 gperftools:

sudo apt install google-perftools

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

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

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
LD_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.c
CPUPROFILE=prof.out ./main.out 10000

另请参阅:gperftools - profile file not dumped

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

google-pprof --callgrind main.out prof.out  > callgrind.out
kcachegrind 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 samples
     187 100.0% 100.0%      187 100.0% common
       0   0.0% 100.0%      187 100.0% __libc_start_main
       0   0.0% 100.0%      187 100.0% _start
       0   0.0% 100.0%        4   2.1% fast
       0   0.0% 100.0%      187 100.0% main
       0   0.0% 100.0%      183  97.9% maybe_slow

另见:How to use google perf tools

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

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

这对大多数人来说可能太硬核了,但它很有趣。最小可运行示例:Quick way to count number of instructions executed in a C program

英特尔 VTune

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

这似乎是封闭源代码且仅限 x86,但从我所听到的情况来看,它可能是惊人的。我不确定它的使用免费程度,但它似乎可以免费下载。 TODO 评估。

在 Ubuntu 18.04、gprof2dot 2019.11.30、valgrind 3.13.0、perf 4.15.18、Linux 内核 4.15.0、FLameGraph 1a0dc6985aad06e76857cf2a354bd5ba0c9ce96b、gperftools 2.5-2 中测试。

【讨论】:

默认 perf 记录使用帧指针寄存器。现代编译器不记录帧地址,而是将寄存器用作通用目的。另一种方法是使用-fno-omit-frame-pointer 标志编译或使用其他替代方法:根据您的情况使用--call-graph "dwarf"--call-graph "lbr" 记录。【参考方案19】:

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

只是认为你在运动时有障碍物,那么它会降低你的速度

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

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

尚未尝试过,但我听说过有关 google-perftools 的好消息。绝对值得一试。

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

它将生成一个名为 gmon.out 或 callgrind.out.x 的文件。然后,您可以使用 kcachegrind 或调试器工具来读取此文件。它将为您提供图形分析结果,例如哪些线路的成本是多少。

我也这么认为

【讨论】:

我实际上建议添加一些优化标志,例如编译g++ -O -pg -Wall your_prg.cpp

以上是关于如何分析在 Linux 上运行的 C++ 代码?的主要内容,如果未能解决你的问题,请参考以下文章

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

什么是 Linux 上易于使用的 C++ 分析器? [关闭]

C++ 程序在 Linux 上完美运行,但不能在 Windows 上运行

C++ 分析和优化

在 Windows 上运行并生成 Linux 代码的 C++ 编译器

用于 C++ 分析的非常困和 Callgrind 之间的区别