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

问:

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

答1:

huntsbot.com洞察每一个产品背后的需求与收益,从而捕获灵感

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

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

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

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

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

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

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

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) 是 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)) 个样本上。例如,{5 },f=0.3,即 3+/-1.4 个样本。)

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

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

https://i.stack.imgur.com/FpWuS.png

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

这基本上是一个穷人的抽样分析器,这很好,但是你冒着样本量太小的风险,这可能会给你带来完全虚假的结果。

@Crash:我不会辩论“穷人”部分:-) 确实,统计测量精度需要很多样本,但是有两个相互矛盾的目标——测量和问题定位。我专注于后者,为此您需要位置精度,而不是测量精度。因此,例如,在堆栈中间,可以有一个函数调用 A();这占了 50% 的时间,但它可以在另一个大函数 B 中,以及对 A() 的许多其他不昂贵的调用。函数时间的精确摘要可能是一个线索,但每个其他堆栈样本都会查明问题所在。

......世界似乎认为带有调用计数和/或平均时间注释的调用图就足够了。它不是。可悲的是,对于那些对调用堆栈进行采样的人来说,最有用的信息就在他们面前,但为了“统计”,他们把它扔掉了。

我并不是要不同意你的技术。显然,我非常依赖堆栈遍历采样分析器。我只是指出,现在有一些工具可以自动执行此操作,当您超过将功能从 25% 降至 15% 并需要将其从 1.2% 降至0.6%。

-1:不错的想法,但如果你在一个以绩效为导向的环境中工作而获得报酬,这就是浪费每个人的时间。使用真正的分析器,这样我们就不必在您身后解决实际问题。

答2:

与HuntsBot一起,探索全球自由职业机会–huntsbot.com

将 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

要记住的一件事是编译包含调试符号但进行优化,以获得与实际“发布”构建相似的速度特征的可探索性。

答3:

与HuntsBot一起,探索全球自由职业机会–huntsbot.com

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

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

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

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

相关问题 here。

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

我同意 gprof 是当前标准。不过请注意,Valgrind 用于分析程序的内存泄漏和其他与内存相关的方面,而不是用于速度优化。

比尔,在 vaglrind 套件中,您可以找到 callgrind 和 massif。两者都对配置应用程序非常有用

@Bill-the-Lizard:关于 gprof 的一些评论:stackoverflow.com/questions/1777556/alternatives-to-gprof/…

gprof -pg 只是调用堆栈分析的近似值。它插入 mcount 调用以跟踪哪些函数正在调用哪些其他函数。它使用基于标准时间的采样,呃,时间。然后它将在函数 foo() 中采样的时间分配给 foo() 的调用者,与调用次数成比例。所以它不区分不同成本的调用。

对于 clang/clang++,可以考虑使用 gperftools 的 CPU 分析器。警告:我自己没有这样做。

答4:

保持自己快人一步,享受全网独家提供的一站式外包任务、远程工作、创意产品订阅服务–huntsbot.com

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

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

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

https://i.stack.imgur.com/FMYp4.png

很棒的工具!无论如何,我是否可以获得从“main->func1->fun2”样式开始的典型“蝴蝶”视图?我似乎无法弄清楚...... perf report 似乎给了我调用父母的函数名称......(所以它有点像倒置的蝴蝶视图)

请问,可以perf显示线程活动的时间表;添加了 CPU 编号信息?我想查看每个 CPU 上运行的时间和线程。

@kizzx2 - 您可以使用 gprof2dot 和 perf 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 浏览器打开交互式图形!

答5:

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

如果没有一些选项,运行 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 对于强制将数据转储到磁盘很有用。

不能。我通常的上下文是整个 MySQL 或 PHP 或类似的大东西。往往一开始甚至不知道自己要分开什么。

或者在我的情况下,我的程序实际上将一堆数据加载到 LRU 缓存中,我不想对其进行分析。因此,我在启动时强制加载缓存的子集,并仅使用该数据分析代码(让 OS+CPU 管理我缓存中的内存使用)。它可以工作,但是在我试图在不同上下文中分析的代码中加载该缓存很慢并且 CPU 密集,因此 callgrind 会产生严重污染的结果。

还有 CALLGRIND_TOGGLE_COLLECT 以编程方式启用/禁用收集;见stackoverflow.com/a/13700817/288875

@TõnuSamuel,对我来说 callgrind.out.* 也是空的。就我而言,程序在分析时崩溃了。一旦崩溃的原因得到解决,我就可以看到 callgrind.out.* 文件中的内容。

答6:

huntsbot.com – 程序员副业首选,一站式外包任务、远程工作、创意产品分享订阅平台。

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

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

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

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

答7:

huntsbot.com – 高效赚钱,自由工作

这是对 Nazgob’s Gprof answer 的回应。

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

它不能在多线程代码上正常工作,除非您使用解决方法调用图被函数指针混淆。示例:我有一个名为 multithread() 的函数,它使我能够在指定数组上对指定函数进行多线程处理(均作为参数传递)。然而,Gprof 将所有对 multithread() 的调用视为计算在子项中花费的时间的等价物。由于我传递给 multithread() 的某些函数比其他函数花费的时间要长得多,所以我的调用图大多没用。 (对于那些想知道线程是否是这里的问题的人:不, multithread() 可以选择,并且在这种情况下确实只在调用线程上按顺序运行所有内容)。它在这里说“…呼叫次数数字是通过计数而不是抽样得出的。它们是完全准确的…”。然而,我发现我的调用图给了我 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) 部分中的引用,但也说“在多线程中“

我不清楚这是否解释了我在 (iii) 中的结果。我的代码链接了 -lpthread -lm 并声明了“pthread_t *thr”和“pthread_mutex_t nextLock = PTHREAD_MUTEX_INITIALIZER”静态变量,即使它运行的是单线程。我通常会假设“与多线程库的链接”意味着实际使用这些库,并且比这更大,但我可能错了!

答8:

huntsbot.com全球7大洲远程工作机会,探索不一样的工作方式

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

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

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

main 调用 fast 和 maybe_slow 3 次,其中一个 maybe_slow 调用很慢 如果我们认为对子函数的调用很常见,那么 may_slow 的慢调用要长 10 倍,并且在运行时占主导地位。理想情况下,分析工具将能够为我们指出特定的慢速调用。

fast 和 maybe_slow 调用都很常见,这占程序执行的大部分

程序接口是:./main.out [n [seed]],程序总共做了 O(n^2) 次循环。种子只是为了在不影响运行时获得不同的输出。

主程序

#include 
#include 
#include 

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 的执行时间是相同的,这很好:没有减速!然而,我已经看到复杂软件(例如 shown in this ticket)的 2x - 3x 减速的帐户。

因为我们使用 -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 运行,我们观察到以下情况:

https://i.stack.imgur.com/mM8NQ.png

对于 -O3 运行:

https://i.stack.imgur.com/31VNy.png

-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 中获取的“真实”软件示例中查看此图像:

https://i.stack.imgur.com/Nvg9G.jpg

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

在 kcachegrind 中查看 gprof 输出

哪个是 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]
-----------------------------------------------
                                                 
[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]
-----------------------------------------------
                                                 
[2]     97.6    0.00    1.79                 maybe_slow [2]
                1.79    0.00  120000/123003      common [1]
-----------------------------------------------
                                                 
[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_slow 和 fast 没有已知的父级,这就是文档所说的 的含义。

我不确定是否有使用 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 而失败,是的,这很常见,以至于我这样做了,并且有一个 Stack Overflow 问题。

所以我们编译并运行为:

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. 的配置文件数据文件,例如在我的例子中是 callgrind.out.8554。我们通过以下方式查看该文件:

kcachegrind callgrind.out.8554

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

https://i.stack.imgur.com/v1kfK.png

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

https://i.stack.imgur.com/ZTdAJ.png

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

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

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_slow和fast速度太快,没有得到任何样本吗?在需要较长时间执行的大型程序上,它是否与 -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 时,我得到了 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_slow 和 fast:

https://i.stack.imgur.com/QFKSS.png

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

https://i.stack.imgur.com/4Ufpd.png

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

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

Eclipse Trace Compass 插件:https://www.eclipse.org/tracecompass/ 但这有一个缺点是您必须先将数据转换为 Common Trace 格式,这可以通过 perf data --to-ctf 来完成,但是它需要在构建时启用/具有足够新的性能,Ubuntu 18.04 中的性能都不是这种情况

https://github.com/KDAB/hotspot 这样做的缺点是似乎没有 Ubuntu 软件包,并且构建它需要 Qt 5.10,而 Ubuntu 18.04 是 Qt 5.9。但是 David Faure 在评论中提到没有 AppImage 包可能是一种方便的使用方式。

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

https://i.stack.imgur.com/SiISF.png

与其他工具一样,它提供了一个熟悉的调用图,但使用的是样本数而不是秒数的笨重单位。

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

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 中测试。

默认情况下,性能记录使用帧指针寄存器。现代编译器不记录帧地址,而是将寄存器用作通用用途。替代方法是使用 -fno-omit-frame-pointer 标志编译或使用不同的替代方法:根据您的方案使用 --call-graph "dwarf" 或 --call-graph "lbr" 记录。

如今,KDAB 的热点附带了一个 AppImage,使其非常易于使用。

答9:

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

使用 Valgrind、callgrind 和 kcachegrind:

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

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

使用 gprof(添加 -pg):

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

(对多线程、函数指针不太好)

使用谷歌性能工具:

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

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

其他:AMD Codeanalyst(后来被 AMD CodeXL 取代)、OProfile、“perf”工具(apt-get install linux-tools)

答10:

huntsbot.com提供全网独家一站式外包任务、远程工作、创意产品分享与订阅服务!

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

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

它看起来很有趣,但无法使用 GCC 9.2 进行编译。 (Debian/Sid) 我在 github 上提出了一个问题。

答11:

huntsbot.com洞察每一个产品背后的需求与收益,从而捕获灵感

另外值得一提的是

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

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

原文链接:https://www.huntsbot.com/qa/9r1E/how-do-i-profile-c-code-running-on-linux?lang=zh_CN&from=csdn

huntsbot.com聚合了超过10+全球外包任务平台的外包需求,寻找外包任务与机会变的简单与高效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值