我有一个正在Linux上运行的C ++应用程序,我正在对其进行优化。如何确定我的代码哪些区域运行缓慢?
code
探查器。但是,优先级倒置,缓存别名,资源争用等可能都是优化和性能方面的因素。我认为人们将信息读入了我的慢速代码。常见问题解答引用了该线程。
我有一个正在Linux上运行的C ++应用程序,我正在对其进行优化。如何确定我的代码哪些区域运行缓慢?
code
探查器。但是,优先级倒置,缓存别名,资源争用等可能都是优化和性能方面的因素。我认为人们将信息读入了我的慢速代码。常见问题解答引用了该线程。
Answers:
如果您的目标是使用探查器,请使用建议的探查器之一。
但是,如果您急于在主观上很慢的情况下在调试器下手动中断程序,则有一种简单的方法可以查找性能问题。
暂停几次,每次查看调用堆栈。如果有一些代码浪费了一定比例的时间(20%或50%或其他),那么这就是您在每次采样时都将其捕获的概率。因此,这大约是您将看到样品的百分比。不需要有根据的猜测。如果您确实怀疑问题出在哪里,这将证明或不证明它。
您可能会遇到多个不同大小的性能问题。如果您清除其中任何一个,其余的将在以后的传递中占更大的比例,并且更容易发现。当放大多个问题时,这种放大效果会导致真正巨大的加速因素。
警告:除非他们自己使用过,否则程序员往往会对这种技术持怀疑态度。他们会说探查器会为您提供此信息,但是只有当他们对整个调用堆栈进行采样,然后让您检查随机的一组采样时,这才是正确的。(摘要是丢失洞察力的地方。)调用图不会为您提供相同的信息,因为
他们还会说,它实际上仅对玩具程序有效,而实际上对任何程序都有效,并且似乎在较大的程序上效果更好,因为它们往往会发现更多的问题。他们会说有时发现没有问题的东西,但这只有在您看到一次之后才是真的。如果您在多个样本上发现问题,那是真实的。
PS如果可以像Java中那样在某个时间点收集线程池的调用堆栈样本,也可以在多线程程序上完成。
PPS大致来说,软件中的抽象层越多,您越有可能发现这是性能问题的原因(并且有提高速度的机会)。
补充:可能并不明显,但是在有递归的情况下,堆栈采样技术同样有效。原因是通过删除一条指令可以节省的时间大约等于包含一条指令的样本所占的比例,而与该指令在一个样本中可能发生的次数无关。
我经常听到的另一个反对意见是:“ 它将在某个地方随机停止,并且将错过真正的问题 ”。这源于对实际问题有一个先验的概念。性能问题的一个关键特性是它们无法兑现预期。抽样告诉您某些问题,而您的第一个反应是难以置信。那是很自然的,但是您可以确定它是否发现了真正的问题,反之亦然。
补充:让我对它的工作原理进行贝叶斯解释。假设有一些指令I
(调用或其他方式)在调用堆栈f
上占时间的一部分(因此花费了很多)。为简单起见,假设我们不知道这f
是什么,但是假设它是0.1、0.2、0.3,... 0.9、1.0,并且每种可能性的先验概率为0.1,因此所有这些成本都是一样的先验的。
然后假设我们只取了两个堆栈样本,并且我们看到I
了两个样本的指示,即指定的观察值o=2/2
。这给我们的频率的新的估计f
的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=10
,f=0.3
,即3+/-1.4
样品)。
补充:为了直观地了解测量和随机堆栈采样之间的区别:
现在有分析器可以对堆栈进行采样,即使是在墙上时钟时间,但结果是测量(或热路径或热点) “瓶颈”很容易隐藏)。他们没有向您显示(并且很容易做到)是实际样本本身。而且,如果您的目标是找到瓶颈,那么平均而言,您需要查看的瓶颈数量为2除以所需的时间。因此,如果花费30%的时间,则平均将显示2 / .3 = 6.7个样本,而20个样本将显示99.2%的机会。
这是检查测量值和检查堆栈样本之间差异的现成插图。瓶颈可能是这样的一个大斑点,也可能是很多小的斑点,这没有什么区别。
测量是水平的;它告诉您特定例程花费的时间比例。采样是垂直的。如果有什么办法可以避免整个程序在那时的工作,并且在第二个示例中看到它,那么您已经找到了瓶颈。这就是与众不同的原因-查看花费时间的全部原因,而不仅仅是花多少时间。
您可以将Valgrind与以下选项一起使用
valgrind --tool=callgrind ./(Your binary)
它将生成一个名为的文件callgrind.out.x
。然后,您可以使用kcachegrind
工具读取此文件。它将为您提供图形化的事物分析结果,例如哪些行花费多少。
./gprof2dot.py -f callgrind callgrind.out.x | dot -Tsvg -o output.svg
gprof2dot
现在在这里:github.com/jrfonseca/gprof2dot
我认为您正在使用GCC。标准解决方案是使用gprof进行分析。
-pg
在进行概要分析之前,请确保添加到编译中:
cc -o myprog myprog.c utils.c -g -pg
我还没有尝试过,但是我听说过有关google-perftools的好消息。绝对值得一试。
相关问题在这里。
较新的内核(例如,最新的Ubuntu内核)随附了新的“ perf”工具(apt-get install linux-tools
)AKA perf_events。
重要的是,这些工具可以是系统配置文件,而不仅仅是进程配置文件-它们可以显示线程,进程和内核之间的交互,并让您了解进程之间的调度和I / O依赖关系。
perf report
似乎给了我带有调用父级的函数名称...(所以它有点像蝴蝶视图)
gprof2dot
和perf script
。很好的工具!
perf
存在于archive.li/9r927#selection-767.126-767.271 (为什么SO神决定从SO知识库中删除该页面超出了我的范围....)
我将使用Valgrind和Callgrind作为我的分析工具套件的基础。重要的是要知道Valgrind实际上是一个虚拟机:
(维基百科)Valgrind本质上是使用实时(JIT)编译技术(包括动态重新编译)的虚拟机。原始程序中的任何内容都无法直接在主机处理器上运行。相反,Valgrind首先将程序转换为称为中间表示(IR)的临时,简单形式,该形式是与处理器无关的,基于SSA的形式。转换之后,在Valgrind将IR转换回机器代码并让主处理器运行它之前,可以使用一种工具(见下文)在IR上进行任何所需的转换。
Callgrind是基于此的探查器。主要好处是您不必花费数小时即可完成可靠的结果。因为Callgrind 是非探测轮廓仪,所以即使是一秒钟的运行也足以获得坚如磐石的可靠结果。
基于Valgrind的另一个工具是Massif。我用它来分析堆内存使用情况。效果很好。它的作用是为您提供内存使用情况的快照-详细信息什么占内存的百分比以及WHO已将其放在那儿。此类信息在应用程序运行的不同时间点可用。
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”显示每个函数本身花费的时间,而不是与从属一起花费的时间。
CALLGRIND_TOGGLE_COLLECT
通过编程方式启用/禁用收集;参见stackoverflow.com/a/13700817/288875
最近几天我一直在使用Gprof,并且已经发现了三个重要的局限性,其中一个我还没有见过其他记录(到目前为止):
除非您使用解决方法,否则它在多线程代码上无法正常工作
调用图被函数指针弄糊涂了。示例:我有一个调用的函数multithread()
,该函数使我可以在指定的数组(都作为参数传递)上对指定的函数进行多线程处理。但是,Gprof multithread()
出于计算儿童所用时间的目的,将所有调用均视为等效。由于我传递的某些功能multithread()
比其他函数花费的时间长得多,因此我的调用图几乎没有用。(让那些想知道线程是否是这里的问题的人:不,multithread()
可以(可选),并且在这种情况下确实可以在调用线程上按顺序运行所有内容)。
它在这里说:“ ...呼叫次数数字是通过计数而不是抽样得出的。它们是完全准确的...”。但是我发现我的调用图给我5345859132 + 784984078作为我最被调用函数的调用统计信息,其中第一个数字应该是直接调用,而第二个递归调用(全部来自其本身)。由于这暗示我有一个错误,因此我在代码中放入了长(64位)计数器,然后再次执行相同的操作。我的计数是:直接5345859132,以及78094395406自递归调用。那里有很多数字,所以我要指出,我测量的递归调用为780亿,而Gprof则为7.84亿:相差100倍。两次运行都是单线程且未经优化的代码,一次是编译的-g
,另一次是-pg
。
这是在64位Debian Lenny下运行的GNU Gprof(用于Debian的GNU Binutils)2.18.0.20080103,如果有帮助的话。
使用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)
C ++分析技术概述
在这个答案中,我将使用几种不同的工具来分析一些非常简单的测试程序,以具体比较这些工具的工作方式。
以下测试程序非常简单,并且可以执行以下操作:
main
通话fast
和maybe_slow
3次,一个maybe_slow
呼叫正在缓慢
慢速调用的maybe_slow
时间长10倍,并且如果考虑到对子函数的调用,则占主导地位的运行时common
。理想情况下,性能分析工具可以将我们指向特定的慢速通话。
fast
和maybe_slow
call 两者都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.gprof
,gprof2dot
然后读取该报告以生成图形。
gprof2dot的来源位于:https : //github.com/jrfonseca/gprof2dot
我们观察到以下-O0
运行情况:
和-O3
运行:
该-O0
输出几乎是不言自明的。例如,它显示这3个maybe_slow
调用及其子调用占据了总运行时的97.56%,尽管不带子进程的执行maybe_slow
占了总执行时间的0.00%,即,几乎在该函数上花费的所有时间都花在了儿童电话。
TODO:即使我可以在GDB中看到它,为什么输出中也main
缺少-O3
它bt
?我认为这是因为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>
,我期望函数名称的位置,那是什么意思?我注意到有一个“循环检测”按钮可以将其打开和关闭,但这是什么意思呢?
perf
从 linux-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_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当我跑perf
没有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 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中进行了测试。
-fno-omit-frame-pointer
flag 进行编译或使用其他替代方案:使用--call-graph "dwarf"
或--call-graph "lbr"
根据您的方案进行记录。
对于单线程程序,可以使用igprof,Ignominous Profiler:https ://igprof.org/ 。
它是一个采样探查器,遵循... long ...的答案,由Mike Dunlavey回答,它将把结果包装在可浏览的调用堆栈树中,并在每个函数中花费的时间或内存进行注释,无论是累积的还是每个功能。
还值得一提的是
我使用过HPCToolkit和VTune,它们在查找帐篷中的长杆时非常有效,不需要重新编译代码(除了必须使用-g -O或CMake中的RelWithDebInfo类型构建以获取有意义的输出) 。我听说TAU的功能类似。
这是我用来加速代码的两种方法:
对于受CPU约束的应用程序:
对于I / O绑定的应用程序:
NB
如果您没有探查器,请使用穷人的探查器。调试应用程序时,请按一下暂停。大多数开发人员套件都会使用带注释的行号来分解成汇编。从统计上看,您很可能会进入一个消耗大部分CPU周期的区域。
对于CPU,在DEBUG模式下进行性能分析的原因是,如果在RELEASE模式下尝试进行性能分析,则编译器将减少数学运算,向量化循环和内联函数,这在汇编代码时往往会使您的代码陷入无法映射的混乱状态。不可映射的混乱意味着您的探查器将无法清楚地识别花费了很长时间的内容,因为程序集可能与优化后的源代码不符。如果您需要RELEASE模式的性能(例如,定时敏感),请根据需要禁用调试器功能以保持可用的性能。
对于受I / O限制的事件,探查器仍可以在RELEASE模式下识别I / O操作,因为I / O操作(大多数情况下)是外部链接到共享库的,或者在最坏的情况下,将导致系统操作失败。调用中断向量(事件探查器也可以轻松识别)。
您可以使用iprof库:
https://gitlab.com/Neurochrom/iprof
https://github.com/Neurochrom/iprof
它是跨平台的,因此您不必实时评估应用程序的性能。您甚至可以将其与实时图形耦合。完全免责声明:我是作者。
在工作中,我们有一个非常不错的工具,可以帮助我们监控调度方面的需求。这已经有用了很多次了。
它是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
函数使用几条组装线尽快获取时钟时间戳,然后在缓冲区中设置一个条目。我们还有一个原子增量,可以安全地找到存储日志事件的索引。当然缓冲区是循环的。
希望该示例不会因缺少示例代码而被混淆。
实际上,关于google / benchmark的提及并不多,这使您感到惊讶,而固定代码的特定区域有些麻烦,特别是在代码库较大的情况下,但是与组合使用时,这确实很有帮助callgrind
恕我直言,找出导致瓶颈的部分是这里的关键。但是,我会先尝试回答以下问题,然后根据该问题选择工具
valgrind
结合使用,callrind
并且kcachegrind
应该对以上几点提供一个不错的估计,一旦确定某些代码段存在问题,我建议您做一个微型基准google benchmark
是一个不错的起点。
-pg
在编译和链接代码并运行可执行文件时使用标志。执行此程序时,分析数据收集在文件a.out中。
有两种不同类型的分析
1-平面剖析:
通过运行命令,gprog --flat-profile a.out
您可以获得以下数据
-该功能花费了总时间的百分比,
- 该功能花费了多少秒-包括但不包括对子功能的调用,
-通话
-每个通话的平均时间。
2-图形分析
命令gprof --graph a.out
为每个功能获取以下数据,其中包括:
-在每个部分中,一个功能都标有索引号。
-在函数上方,有一个调用函数的函数列表。
-在函数下方,有函数调用的函数列表。
使用调试软件 如何识别代码在哪里运行缓慢?
只是认为自己在运动中遇到障碍,就会降低速度
像不必要的重新分配的循环,缓冲区溢出,搜索,内存泄漏等操作消耗更多的执行能力,这会对代码的性能产生不利影响。请确保在进行性能分析之前将-pg添加到编译中:
g++ your_prg.cpp -pg
或cc my_program.cpp -g -pg
根据您的编译器
尚未尝试过,但我听说过有关google-perftools的好消息。绝对值得一试。
valgrind --tool=callgrind ./(Your binary)
它将生成一个名为gmon.out或callgrind.out.x的文件。然后,您可以使用kcachegrind或调试器工具读取此文件。它将为您提供图形化的事物分析结果,例如哪些行花费多少。
我认同