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


1815

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


27
如果您将提供有关开发堆栈的更多数据,则可能会得到更好的答案。有来自Intel和Sun的分析器,但是您必须使用它们的编译器。那是一个选择吗?
纳兹哥布

2
在下面的链接上已经回答了:stackoverflow.com/questions/2497211/…–
卡皮尔·古普塔

4
大多数答案是code探查器。但是,优先级倒置,缓存别名,资源争用等可能都是优化和性能方面的因素。我认为人们将信息读入了我的慢速代码。常见问题解答引用了该线程。
无声的噪音,


3
我以前是随机使用pstack的,大多数时候大多数情况下会打印出程序所在的最典型的堆栈,从而指出了瓶颈。
何塞·曼努埃尔·戈麦斯·阿尔瓦雷斯

Answers:


1404

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

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

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

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

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

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

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

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

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

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

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

补充:让我对它的工作原理进行贝叶斯解释。假设有一些指令I(调用或其他方式)在调用堆栈f上占时间的一部分(因此花费了很多)。为简单起见,假设我们不知道这f是什么,但是假设它是0.1、0.2、0.3,... 0.9、1.0,并且每种可能性的先验概率为0.1,因此所有这些成本都是一样的先验的。

然后假设我们只取了两个堆栈样本,并且我们看到I了两个样本的指示,即指定的观察值o=2/2。这给我们的频率的新的估计fI,按照这样的:

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。如果数据量很小,它并不能准确地告诉我们成本是多少,而只是告诉我们它足够大才能值得解决。

另一种看待它的方法称为继承规则。如果您掷硬币两次,并且两次都出现正面,那么这可能告诉您硬币的权重是多少?尊重的回答方式是说它是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))样品。实施例,n=10f=0.3,即3+/-1.4样品)。


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

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

在此处输入图片说明

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


292
这基本上是一个穷人的采样分析器,虽然很好,但是您冒着样本量过小的风险,这可能会给您带来完全虚假的结果。
Crashworks,2009年

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

41
……世界似乎认为,以通话次数和/或平均时间为注释的通话图就足够了。它不是。可悲的是,对于那些对调用堆栈进行采样的人来说,最有用的信息就在他们眼前,但是为了“统计”的利益,他们却抛弃了这些信息。
Mike Dunlavey,2009年

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

13
-1:好主意,但是如果您获得报酬甚至在中等性能的环境中工作,那都是在浪费每个人的时间。使用真实的探查器,这样我们就不必跟在后面来解决实际的问题。
山姆·哈威尔

583

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

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

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


51
valgrind很棒,但是要注意,它会使程序的速度变慢

30
另请参阅Gprof2Dot,以一种令人惊奇的替代方式可视化输出。./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg
塞巴斯蒂安

2
@neves是的,Valgrind只是在实时分析“ gstreamer”和“ opencv”应用程序的速度方面不是很有帮助。
狂热爱好者

1
stackoverflow.com/questions/375913/…是速度问题的部分解决方案。
托纳·塞缪尔

3
@Sebastian:gprof2dot现在在这里:github.com/jrfonseca/gprof2dot
John Zwinck

347

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

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

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

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

相关问题在这里

如果还有其他一些流行词gprof对您不起作用,那就是:Valgrind,Intel VTune,Sun DTrace


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

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

7
@比尔最蜥蜴:上一些评论gprof的stackoverflow.com/questions/1777556/alternatives-to-gprof/...
麦克Dunlavey

6
gprof -pg只是调用堆栈分析的近似值。它插入mcount调用以跟踪哪些函数正在调用哪些其他函数。它使用基于时间的标准采样来作为时间。然后,它会将在函数foo()中采样的时间分配回给foo()的调用者,以分配给调用的编号。因此,它无法区分不同费用的通话。
Krazy Glew 2012年

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

257

较新的内核(例如,最新的Ubuntu内核)随附了新的“ perf”工具(apt-get install linux-tools)AKA perf_events

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

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

替代文字


12
很棒的工具!无论如何,我有没有从“ main-> func1-> fun2”样式开始的典型“蝴蝶”视图?我似乎无法弄清楚... perf report似乎给了我带有调用父级的函数名称...(所以它有点像蝴蝶视图)
kizzx2 2010年

可以,可以显示线程活动的时间表;加上CPU号信息?我想查看何时以及哪个线程在每个CPU上运行。
osgx 2011年

2
@ kizzx2-您可以使用gprof2dotperf script。很好的工具!
2012年

2
甚至像4.13这样的较新内核也都具有eBPF进行性能分析。参见brendangregg.com/blog/2015-05-15/ebpf-one-small-step.htmlbrendangregg.com/ebpf.html
Andrew Stern

另一个不错的介绍perf存在于archive.li/9r927#selection-767.126-767.271 (为什么SO神决定从SO知识库中删除该页面超出了我的范围....)
ragerdl

75

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

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

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

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


70

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”显示每个函数本身花费的时间,而不是与从属一起花费的时间。


9
现在由于某种原因,callgrind.out。*文件始终为空。执行callgrind_control -d有助于将数据转储到磁盘。
托纳·塞缪尔

3
不行 我通常的情况是整个MySQL或PHP或类似的大事。常常甚至一开始都不知道我要分开的内容。
托纳·塞缪尔

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

2
还可以CALLGRIND_TOGGLE_COLLECT通过编程方式启用/禁用收集;参见stackoverflow.com/a/13700817/288875
Andre Holzner

1
哇,我不知道这个存在,谢谢!
文森特·富蒙德

59

这是对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,如果有帮助的话。


是的,它会进行抽样,但不会提供通话次数数字。有趣的是,按照您的链接,最终使我获得了我在帖子中链接的手册页的更新版本,即新的URL:sourceware.org/binutils/docs/gprof/… 这重复了答案的(iii)部分,但还说:“在多线程应用程序或与多线程库链接的单线程应用程序中,只有在计数函数是线程安全的情况下,计数才是确定的。(注意:请注意,glibc中的mcount计数函数不是线程-安全)。”
Rob_before_edits 2012年

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

23

使用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)


10

C ++分析技术概述

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

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

  • main通话fastmaybe_slow3次,一个maybe_slow呼叫正在缓慢

    慢速调用的maybe_slow时间长10倍,并且如果考虑到对子函数的调用,则占主导地位的运行时common。理想情况下,性能分析工具可以将我们指向特定的慢速通话。

  • fastmaybe_slowcall 两者都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倍,例如此票证所示

由于我们使用编译-pg,因此运行程序将生成一个gmon.out包含概要分析数据的文件文件。

我们可以gprof2dot按以下要求以图形方式观察该文件:是否可以获取gprof结果的图形表示?

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.gprofgprof2dot然后读取该报告以生成图形。

gprof2dot的来源位于:https : //github.com/jrfonseca/gprof2dot

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

在此处输入图片说明

-O3运行:

在此处输入图片说明

-O0输出几乎是不言自明的。例如,它显示这3个maybe_slow调用及其子调用占据了总运行时的97.56%,尽管不带子进程的执行maybe_slow占了总执行时间的0.00%,即,几乎在该函数上花费的所有时间都花在了儿童电话。

TODO:即使我可以在GDB中看到它,为什么输出中也main缺少-O3bt?我认为这是因为Gprof不仅基于编译工具,而且基于采样,而且-O3 main速度太快,没有样本,因此我认为这是因为GProf输出缺少功能

我选择SVG输出而不是PNG,因为SVG可通过Ctrl + F搜索,并且文件大小可以小10倍左右。此外,对于复杂的软件,生成的图像的宽度和高度可能非常庞大,有成千上万的像素,而eog在这种情况下,GNOME 3.28.1会针对PNG进行修正,而SVG会由我的浏览器自动打开。虽然gimp 2.8运作良好,但另请参阅:

但是即使那样,您仍然会拖拉图像很多,以找到所需的内容,例如,从这张票证中获取的“真实”软件示例中的图像:

在此处输入图片说明

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

但是,您可以使用颜色图稍微缓解这些问题。例如,在上一个巨大的图像上,当我做出出色的推论时,我终于设法找到了左边的关键路径,即绿色紧随红色之后,最后是越来越深的蓝色。

另外,我们也可以观察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_slow并且fast没有已知的父级,这就是文档所说的<spontaneous>意思。

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

valgrind callgrind

valgrind通过valgrind虚拟机运行程序。这使得配置文件非常准确,但是也会导致程序运行速度大大降低。我之前在以下地方也提到过kcachegrind:用于获取图形函数的代码调用图的工具

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倍。显然,这种放缓将成为较大工作负载的严重限制。在这里提到的“实际软件示例” ,我观察到速度降低了80倍。

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

kcachegrind callgrind.out.8554

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

在此处输入图片说明

另外,如果我们在右下角的“调用图”选项卡上,我们将看到一个调用图,可以通过右键单击将其导出以获取以下图像,并获得不合理数量的白色边框:-)

在此处输入图片说明

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

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

perflinux-tools

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

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

这增加了0.2s的执行时间,因此我们可以按时使用,但是在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和还提到了他PERF教程:http://www.brendangregg.com/perf.html#FlameGraphs当我跑perf没有sudoERROR: 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在该图中看到:

在此处输入图片说明

在更复杂的示例中,可以清楚地看到图形的含义:

在此处输入图片说明

TODO [unknown]在该示例中有一个功能日志,为什么?

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

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

    但这有一个缺点,那就是您必须首先将数据转换为通用跟踪格式,可以使用来完成perf data --to-ctf,但是需要在构建时将其启用/具有perf足够的新性,对于perf in Ubuntu 18.04

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

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

gperftools

以前称为“ Google Performance Tools”,来源: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-未转储配置文件

到目前为止,查看此数据的最好方法是使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

另请参阅:如何使用Google Perf工具

已在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中进行了测试。


2
默认情况下,perf记录使用帧指针寄存器。现代编译器不记录帧地址,而是将寄存器用作常规用途。替代方案是使用-fno-omit-frame-pointerflag 进行编译或使用其他替代方案:使用--call-graph "dwarf"--call-graph "lbr"根据您的方案进行记录。
豪尔赫·贝隆

5

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

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


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

5

还值得一提的是

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

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


4

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

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

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

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

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

NB

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

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

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


2
+1可怜的人的方法对于I / O绑定和CPU绑定同样有效,我建议在DEBUG模式下进行所有性能调整。完成调整后,请打开RELEASE。如果程序在您的代码中受CPU约束,则将有所改善。这是该过程的简短但简短的视频。
Mike Dunlavey 2014年

3
我不会使用DEBUG构建进行性能分析。我经常看到在调试模式下性能至关重要的部分在发布模式下已完全优化。另一个问题是在调试代码中使用断言,这会增加性能。
gast128

3
你看完我的帖子吗?“如果需要RELEASE模式的性能(例如,对时间敏感),请根据需要禁用调试器功能以保持可用的性能”,“然后切换到RELEASE模式并注释代码中有问题的部分(不添加任何内容,直到没有看到)。性能变化。”?我说过在调试模式下检查可能的问题区域,并在发布模式下验证那些问题,以避免您提到的陷阱。
seo 2014年


2

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


1

在工作中,我们有一个非常不错的工具,可以帮助我们监控调度方面的需求。这已经有用了很多次了。

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

您检索具有所有数据的所谓大缓冲区,并通过一个小接口对其进行解析,并显示具有名称(上/下+值)的事件,就像示波器用颜色(在.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函数使用几条组装线尽快获取时钟时间戳,然后在缓冲区中设置一个条目。我们还有一个原子增量,可以安全地找到存储日志事件的索引。当然缓冲区是循环的。

希望该示例不会因缺少示例代码而被混淆。


1

实际上,关于google / benchmark的提及并不多,这使您感到惊讶,而固定代码的特定区域有些麻烦,特别是在代码库较大的情况下,但是与组合使用时,这确实很有帮助callgrind

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

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

valgrind结合使用,callrind并且kcachegrind应该对以上几点提供一个不错的估计,一旦确定某些代码段存在问题,我建议您做一个微型基准google benchmark是一个不错的起点。


1

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

1-平面剖析:
通过运行命令,gprog --flat-profile a.out您可以获得以下数据
-该功能花费了总时间的百分比,
- 该功能花费了多少秒-包括但不包括对子功能的调用,
-通话
-每个通话的平均时间。

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

要获取更多信息,请访问https://sourceware.org/binutils/docs-2.32/gprof/


0

正如没有人提到Arm MAP一样,我会添加它,因为我个人已经成功地使用Map来分析C ++科学程序。

Arm MAP是用于并行,多线程或单线程C,C ++,Fortran和F90代码的探查器。它提供了对源代码行的深入分析和瓶颈。与大多数探查器不同,它旨在针对并行和线程代码探查pthreads,OpenMP或MPI。

MAP是商业软件。


0

使用调试软件 如何识别代码在哪里运行缓慢?

只是认为自己在运动中遇到障碍,就会降低速度

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

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

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

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

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

我认同

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.