2038

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

4

19 回答 19

1540

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

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

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

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

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

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

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

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

PPS作为一个粗略的概括,您的软件中的抽象层越多,您就越有可能发现这是性能问题的原因(以及获得加速的机会)。

补充:这可能并不明显,但堆栈采样技术在存在递归的情况下同样有效。原因是删除一条指令所节省的时间近似为包含它的样本的分数,而不管它在样本中可能出现的次数。

我经常听到的另一个反对意见是:“它会随机停在某个地方,它会错过真正的问题”。这来自对真正问题的先验概念。性能问题的一个关键特性是它们违背预期。抽样告诉你有问题,你的第一反应是不相信。这是自然的,但您可以确定它是否发现问题是真实的,反之亦然。

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

然后假设我们只取了 2 个堆栈样本,并且我们I在两个样本上都看到了指令,指定为观察o=2/2f这为我们提供了 的频率的新估计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.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%。

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

在此处输入图像描述

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

于 2008-12-18T14:36:43.940 回答
652

您可以通过以下选项使用Valgrind

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

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

于 2009-04-21T04:09:20.267 回答
377

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

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

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

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

相关问题在这里

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

于 2008-12-17T20:34:45.107 回答
287

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

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

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

替代文字

于 2010-08-17T11:48:38.520 回答
91

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

于 2012-06-08T08:01:57.377 回答
86

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

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

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

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

于 2009-05-22T21:44:19.863 回答
66

这是对Nazgob 的 Gprof 回答的回应

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

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

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

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

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

于 2011-06-30T19:30:44.547 回答
44

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

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

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

  • main呼叫fastmaybe_slow3 次,其中一个maybe_slow呼叫速度很慢

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

  • 和call fast,它占程序执行的大部分maybe_slowcommon

  • 程序界面为:

    ./main.out [n [seed]]
    

    并且程序O(n^2)总共循环。seed只是为了在不影响运行时获得不同的输出。

主程序

#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:为什么输出中main缺少-O3,即使我可以bt在 GDB 中看到它?GProf 输出中缺少函数我认为这是因为 gprof 除了其编译的仪器外,也是基于采样的,而且-O3 main速度太快并且没有样本。

我选择 SVG 输出而不是 PNG,因为可以使用Ctrl+搜索 SVG,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` time 花在特定代码行上

valgrind 调用研磨

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

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

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

kcachegrind callgrind.out.8554

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

在此处输入图像描述

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

在此处输入图像描述

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

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

perflinux-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 教程中也提到过:httpperf ://www.brendangregg.com/perf.html#FlameGraphs 当我没有跑步时,sudo我得到了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

在此处输入图像描述

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

在此处输入图像描述

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 性能工具”,来源: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 工具

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

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

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

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

于 2020-02-17T15:15:28.173 回答
32

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

于 2017-02-23T21:28:10.530 回答
6

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

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

于 2018-03-17T12:20:45.357 回答
6

另外值得一提的是

  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 或 RelWithDebInfo 类型构建以获得有意义的输出) . 我听说 TAU 的功能相似。

于 2018-09-14T22:56:48.260 回答
4

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

对于 CPU 绑定的应用程序:

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

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

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

注意

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

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

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

于 2013-11-28T18:21:44.820 回答
4

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

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

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

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

于 2019-11-03T14:47:54.107 回答
3

您可以使用 iprof 库:

https://gitlab.com/Neurochrom/iprof

https://github.com/Neurochrom/iprof

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

于 2019-02-24T18:01:00.000 回答
3

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

于 2019-05-21T13:28:31.817 回答
2

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

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

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

于 2019-05-17T10:13:01.050 回答
1

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

1-平面分析:
通过运行命令gprog --flat-profile a.out,您可以获得以下数据
- 函数花费的总时间百分比,
- 在函数中花费的秒数 - 包括和不包括对子函数的调用,
- 的数量呼叫,
- 每次呼叫的平均时间。

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

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

于 2019-12-07T12:52:42.283 回答
1

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

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

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

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

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

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

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

我认同

于 2020-04-18T19:23:34.600 回答
0

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

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

MAP 是商业软件。

于 2019-06-28T04:44:24.423 回答