原文:https://www.ibm.com/developerworks/cn/linux/l-gnuprof.html

使用 GNU profiler 来提高代码运行速度

寻找应用程序中占用时间最长的部分

改进应用程序的性能是一项非常耗时耗力的工作,但是究竟程序中是哪些函数消耗掉了大部分执行时间,这通常都不是非常明显的。在本文中我们将学习如何使用 gprof 为 Linux 平台上的用户空间和系统调用精确分析性能瓶颈。

假设我们现在已经有了一个可以工作的应用程序,接下来让我们来看一下如何使用 gprof 来精确测量应用程序执行过程中时间都花费到什么地方去了,这样做的目的是了解一下在什么地方进行优化效果最佳。

gprof 可以对 C、C++、Pascal 和 Fortran 77 应用程序进行剖析。本文中的例子使用的是 C。

清单 1. 耗时的应用程序示例
#include <stdio.h>
int a(void) {int i=0,g=0;while(i++<100000){g+=i;}return g;
}
int b(void) {int i=0,g=0;while(i++<400000){g+=i;}return g;
}
int main(int argc, char** argv)
{int iterations;if(argc != 2){printf("Usage %s <No of Iterations>\n", argv[0]);exit(-1);}elseiterations = atoi(argv[1]);printf("No of iterations = %d\n", iterations);while(iterations--){a();b();}
}

正如我们从代码中可以看到的,这个非常简单的应用程序包括两个函数:ab,它们都处于一个繁忙的循环中消耗 CPU 周期。main 函数中采用了一个循环来反复调用这两个函数。第二个函数 b 循环的次数是 a 函数的 4 倍,因此我们期望在对代码分析完之后,可以看出大概有 20% 的时间花在了 a 函数中,而 80% 的时间花在了 b 函数中。下面就开始剖析代码,并看一下我们的这些期望是否正确。

启用剖析非常简单,只需要在 gcc 编译标志中加上 -pg 即可。编译方法如下:

gcc example1.c -pg -o example1 -O2 -lc

在编译好这个应用程序之后,可以按照普通方式运行这个程序:

./example1 50000

当这个程序运行完之后,应该会看到在当前目录中新创建了一个文件 gmon.out。

使用输出结果

首先看一下 “flat profile”,我们可以使用 gprof 命令获得它,这需要为其传递可执行文件和 gmon.out 文件作为参数,如下所示:

gprof example1 gmon.out -p

这会输出以下内容:

清单 2. flat profile 的结果
Flat profile:
Each sample counts as 0.01 seconds.%   cumulative   self              self     totaltime   seconds   seconds    calls  ms/call  ms/call  name80.24     63.85    63.85    50000     1.28     1.28  b20.26     79.97    16.12    50000     0.32     0.32  a

从这个输出结果中可以看到,正如我们期望的一样,b 函数所花费的时间大概是 a 函数所花费的时间的 4 倍。真正的数字并不是十分有用;由于取整舍入错误,这些数字可能并不是非常精确。

聪明的读者可能会注意到,很多函数调用(例如 printf)在这个输出中都没有出现。这是因为这些函数都是在 C 运行时库(libc.so)中的,(在本例中)它们都没有使用 -pg 进行编译,因此就没有对这个库中的函数收集剖析信息。稍后我们会回到这个问题上来。

接下来我们希望了解的是 “call graph”,这可以通过下面的方式获得:

gprof example1 gmon.out -q

这会输出下面的结果。

清单 3. Call graph
                     Call graph (explanation follows)
granularity: each sample hit covers 2 byte(s) for 0.01% of 79.97 seconds
index % time    self  children    called     name<spontaneous>
[1]    100.0    0.00   79.97                 main [1]63.85    0.00   50000/50000       b [2]16.12    0.00   50000/50000       a [3]
-----------------------------------------------63.85    0.00   50000/50000       main [1]
[2]     79.8   63.85    0.00   50000         b [2]
-----------------------------------------------16.12    0.00   50000/50000       main [1]
[3]     20.2   16.12    0.00   50000         a [3]
-----------------------------------------------

最后,我们可能会希望获得一个 “带注解的源代码” 清单,它会将源代码输出到应用程序中,并加上每个函数被调用了多少次的注释。

要使用这种功能,请使用启用调试功能的标志来编译源代码,这样源代码就会被加入可执行程序中:

gcc example1.c -g -pg -o example1 -O2 -lc

像以前一样重新运行这个应用程序:

./example1 50000

gprof 命令现在应该是:

gprof example1 gmon.out -A

这会输出下面的结果:

清单 4. 带注释的源代码
*** File /home/martynh/profarticle/example1.c:#include <stdio.h>50000 -> int a(void) {int i=0,g=0;while(i++<100000){g+=i;}return g;}50000 -> int b(void) {int i=0,g=0;while(i++<400000){g+=i;}return g;}int main(int argc, char** argv)##### -> {int iterations;if(argc != 2){printf("Usage %s <No of Iterations>\n", argv[0]);exit(-1);}elseiterations = atoi(argv[1]);printf("No of iterations = %d\n", iterations);while(iterations--){a();b();}}
Top 10 Lines:Line      Count3      5000011      50000
Execution Summary:3   Executable lines in this file3   Lines executed100.00   Percent of the file executed100000   Total number of line executions33333.33   Average executions per line

共享库的支持

正如在前面曾经介绍的,对于代码剖析的支持是由编译器增加的,因此如果希望从共享库(包括 C 库 libc.a)中获得剖析信息,就需要使用 -pg 来编译这些库。幸运的是,很多发行版都提供了已经启用代码剖析支持而编译的 C 库版本(libc_p.a)。

在我使用的发行版 gentoo 中,需要将 “profile” 添加到 USE 标志中,并重新执行 emerge glibc。当这个过程完成之后,就会看到 /usr/lib/libc_p.a 文件已经创建好了。对于没有按照标准提供 libc_p 的发行版本来说,需要检查它是否可以单独安装,或者可能需要自己下载 glibc 的源代码并进行编译。

在获得 libc_p.a 文件之后,就可以简单地重新编译前面的例子了,方法如下:

gcc example1.c -g -pg -o example1 -O2 -lc_p

然后,可以像以前一样运行这个应用程序,并获得 flat profile 或 call graph,应该会看到很多 C 运行函数,包括 printf(这些函数在我们的测试函数中并不是太重要)。

用户时间与内核时间

现在我们已经知道如何使用 gprof 了,接下来可以简单且有效地对应用程序进行分析了,希望可以消除性能瓶颈。

不过现在您可能已经注意到了 gprof 的最大缺陷:它只能分析应用程序在运行 过程中所消耗掉的用户 时间。通常来说,应用程序在运行时既要花费一些时间来运行用户代码,也要花费一些时间来运行 “系统代码”,例如内核系统调用。

如果对清单 1 稍加修改,就可以清楚地看出这个问题:

清单 5. 为清单 1 添加系统调用分析功能
#include <stdio.h>
int a(void) {sleep(1);return 0;
}
int b(void) {sleep(4);return 0;
}
int main(int argc, char** argv)
{int iterations;if(argc != 2){printf("Usage %s <No of Iterations>\n", argv[0]);exit(-1);}elseiterations = atoi(argv[1]);printf("No of iterations = %d\n", iterations);while(iterations--){a();b();}
}

正如您可以看到的,我们对清单 1 中的代码进行了修改,现在 a 函数和 b 函数不再只处理繁忙的循环了,而是分别调用 C 运行时函数 sleep 来挂起执行 1 秒和 4 秒。

像以前一样编译这个应用程序:

gcc example2.c -g -pg -o example2 -O2 -lc_p

并让这个程序循环 30 次:

./example2 30

所生成的 flat profile 如下所示:

清单 6. flat profile 显示了系统调用的结果
Flat profile:
Each sample counts as 0.01 seconds.no time accumulated%   cumulative   self              self     totaltime   seconds   seconds    calls  Ts/call  Ts/call  name0.00      0.00     0.00      120     0.00     0.00  sigprocmask0.00      0.00     0.00       61     0.00     0.00  __libc_sigaction0.00      0.00     0.00       61     0.00     0.00  sigaction0.00      0.00     0.00       60     0.00     0.00  nanosleep0.00      0.00     0.00       60     0.00     0.00  sleep0.00      0.00     0.00       30     0.00     0.00  a0.00      0.00     0.00       30     0.00     0.00  b0.00      0.00     0.00       21     0.00     0.00  _IO_file_overflow0.00      0.00     0.00        3     0.00     0.00  _IO_new_file_xsputn0.00      0.00     0.00        2     0.00     0.00  _IO_new_do_write0.00      0.00     0.00        2     0.00     0.00  __find_specmb0.00      0.00     0.00        2     0.00     0.00  __guard_setup0.00      0.00     0.00        1     0.00     0.00  _IO_default_xsputn0.00      0.00     0.00        1     0.00     0.00  _IO_doallocbuf0.00      0.00     0.00        1     0.00     0.00  _IO_file_doallocate0.00      0.00     0.00        1     0.00     0.00  _IO_file_stat0.00      0.00     0.00        1     0.00     0.00  _IO_file_write0.00      0.00     0.00        1     0.00     0.00  _IO_setb0.00      0.00     0.00        1     0.00     0.00  ____strtol_l_internal0.00      0.00     0.00        1     0.00     0.00  ___fxstat640.00      0.00     0.00        1     0.00     0.00  __cxa_atexit0.00      0.00     0.00        1     0.00     0.00  __errno_location0.00      0.00     0.00        1     0.00     0.00  __new_exitfn0.00      0.00     0.00        1     0.00     0.00  __strtol_internal0.00      0.00     0.00        1     0.00     0.00  _itoa_word0.00      0.00     0.00        1     0.00     0.00  _mcleanup0.00      0.00     0.00        1     0.00     0.00  atexit0.00      0.00     0.00        1     0.00     0.00  atoi0.00      0.00     0.00        1     0.00     0.00  exit0.00      0.00     0.00        1     0.00     0.00  flockfile0.00      0.00     0.00        1     0.00     0.00  funlockfile0.00      0.00     0.00        1     0.00     0.00  main0.00      0.00     0.00        1     0.00     0.00  mmap0.00      0.00     0.00        1     0.00     0.00  moncontrol0.00      0.00     0.00        1     0.00     0.00  new_do_write0.00      0.00     0.00        1     0.00     0.00  printf0.00      0.00     0.00        1     0.00     0.00  setitimer0.00      0.00     0.00        1     0.00     0.00  vfprintf0.00      0.00     0.00        1     0.00     0.00  write

如果对这个输出结果进行分析,我们就会看到,尽管 profiler 已经记录了每个函数被调用的确切次数,但是为这些函数记录的时间(实际上是所有函数)都是 0.00。这是因为 sleep 函数实际上是执行了一次对内核空间的调用,从而将应用程序的执行挂起了,然后有效地暂停执行,并等待内核再次将其唤醒。由于花在用户空间执行的时间与花在内核中睡眠的时间相比非常小,因此就被取整成零了。其原因是 gprof 仅仅是通过以固定的周期对程序运行时间 进行采样测量来工作的。因此,当程序不运行时,就不会对程序进行采样测量。

这实际上是一把双刃剑。从一个方面来说,这使得有些程序非常难以进行优化,例如花费大部分时间在内核空间的程序,或者由于外部因素(例如操作系统的 I/O 子系统过载)而运行得非常慢的程序。从另一个方面来说,这意味着剖析不会受到系统中其他事件的影响(例如另外一个用户使用了大量的 CPU 时间)。

通常,有一个很好的基准测试可以用来查看 gprof 对于帮助对应用程序进行优化是多么有用,方法是在 time 命令下面执行它。这个命令会显示一个应用程序运行完成需要多少时间,并可以测量它在用户空间和内核空间各花费了多少时间。

如果查看一下清单 2 中的例子:

time ./example2 30

输出结果应该如下所示:

清单 7. time 命令的输出结果
No of iterations = 30
real    2m30.295s
user    0m0.000s
sys     0m0.004s

我们可以看出几乎没有多少时间被花费在执行用户空间的代码上,因此 gprof 在此处不会非常有用。

结束语

尽管 gprof 存在上面的限制,但是它对于优化代码来说依然是个非常有用的工具,如果您的代码大部分是用户空间 CPU 密集型的,它的用处就更加明显。首先使用 time 来运行程序从而判断 gprof 是否能产生有用信息是个好主意。

如果 gprof 不适合您的剖析需要,那么还有其他一些工具可以克服 gprof 部分缺陷,包括 OProfile 和 Sysprof (请参看 参考资料 中有关这些工具信息的链接)。

从另一个方面来说,假设我们已经安装了 gcc,gprof 相对于其他工具来说,一个主要的优点是很可能早已在 Linux 机器上安装了需要使用的工具。

这是参考另一篇博文的:

有些时候,我们特别关注程序的性能,特别是底层软件,比如驱动程序,OS等。为了更好的优化程序性能,我们必须找到性能瓶颈点,“好钢用在刀刃上”才能取 得好的效果,否则可能白做工作。为了找到关键路径,我们可以使用profilng技术,在Linux平台上,我们可以使用gprof和oprofile工 具。

  • gprof是GNU工具之一,它在编译的时候在每个函数的出入口加入了profiling的代码,运行时统计程序在用户态的 执行信息,可以得到每个函数的调用次数,执行时间,调用关系等信息,简单易懂。适合于查找用户级程序的性能瓶颈,对于很多时间都在内核态执行的程 序,gprof不适合。
  • oprofile也是一个开源的profiling工具,它使用硬件调试寄存器来统计信息,进 行profiling的开销比较小,而且可以对内核进行profiling。它统计的信息非常的多,可以得到cache的缺失率,memory的访存信 息,分支预测错误率等等,这些信息gprof是得不到的,但是对于函数调用次数,它是不能够得到的。。

简单来说,gprof简单,适合于查找用户级程序的瓶颈,而oprofile稍显复杂,但是得到的信息更多,更适合调试系统软件。
    我们以编译运行hello.c为例,来说明如何使用这两个工具,这里不解释具体结果的含义,要想详细了解每个结果代表什么意思,可以看一下参考资料中官方站点上的doc信息,里面会给你详尽的解释。
gprof Quick Start
    gprof是gnu binutils工具之一,默认情况下linux系统当中都带有这个工具。

  1. 使用 -pg 选项来编译hello.c,如果要得到带注释的源码清单,则需要增加 -g 选项。运行: gcc -pg -g -o hello hello.c
  2. 运行应用程序: ./hello 会在当前目录下产生gmon.out文件
  3. 使用gprof来分析gmon.out文件,需要把它和产生它的应用程序关联起来:
    1. gprof hello gmon.out -p 得到每个函数占用的执行时间
    2. gprof hello gmon.out -q 得到call graph,包含了每个函数的调用关系,调用次数,执行时间等信息。
    3. gprof hello gmon.out -A 得到一个带注释的“源代码清单”,它会注释源码,指出每个函数的执行次数。这需要在编译的时候增加 -g选项。

oprofile Quick Start
    oprofile是sourceforge上面的一个开源项目,在2.6内核上带有这个工具,好像只有smp系统才有。比较老的系统,需要自己安装,重新编译内核。
    oprofile是一套工具,分别完成不同的事情。

op_help: 列出所有支持的事件。
opcontrol:设置需要收集的事件。
opreport: 对结果进行统计输出。
opannaotate:产生带注释的源/汇编文件,源语言级的注释需要编译源文件时的支持。
opstack:    产生调用图profile,但要求x86/2.6的平台,并且linux2.6安装了call-graph patch
opgprof:    产生如gprof相似的结果。
oparchive: 将所有的原始数据文件收集打包,可以到另一台机器上进行分析。
op_import: 将采样的数据库文件从另一种abi转化成本地格式。

运行oprofile需要root权限,因为它要加载profile模块,启动oprofiled后台程序等。所以在运行之前,就需要切换到root。

  1. opcontrol --init 加载模块,mout /dev/oprofile 创建必需的文件和目录
  2. opcontrol --no-vmlinux 或者 opcontrol --vmlinux=/boot/vmlinux-`uname -r` 决定是否对kernel进行profiling
  3. opcontrol --reset 清楚当前会话中的数据
  4. opcontrol --start 开始profiling
  5. ./hello 运行应用程序,oprofile会对它进行profiling
  6. opcontrol --dump 把收集到的数据写入文件
  7. opcontrol --stop 停止profiling
  8. opcotrol -h 关闭守护进程oprofiled
  9. opcontrol --shutdown 停止oprofiled
  10. opcontrol --deinit 卸载模块

常用的是3→7这几个过程,得到性能数据之后,可以使用opreport, opstack, opgprof, opannotate几个工具进行分析,我常用的是opreport, opannotate进行分析。

  1. opreport使用 http://oprofile.sourceforge.net/doc/opreport.html
  2. opannotate使用 http://oprofile.sourceforge.net/doc/opannotate.html
  3. opgprof使用 http://oprofile.sourceforge.net/doc/opgprof.html

最常用的是opreport,这个可以给出image和symbols的信息,比如我想得到每个函数的执行时间占用比例等信息,用来发现系统性能瓶颈。opannotate可以对源码进行注释,指出哪个地方占用时间比较多。常用命令如下:

  • opreport -l /bin/bash --exclude-depand --threshold 1 , 用来发现系统瓶颈。
  • opannotate --source --output-dir=annotated /usr/local/oprofile-pp/bin/oprofiled
  • opannotate --source --base-dirs=/tmp/build/libfoo/ --search-dirs=/home/user/libfoo/ --output-dir=annotated/ /lib/libfoo.so

网络资源

  1. gprof 用户手册 http://sourceware.org/binutils/docs-2.17/gprof/index.html
  2. oprofile官方站点 http://oprofile.sourceforge.net/
  3. 使用 GNU profiler 来提高代码运行速度 http://www-128.ibm.com/developerworks/cn/linux/l-gnuprof.html
  4. 使用 OProfile for Linux on POWER 识别性能瓶颈 http://www-128.ibm.com/developerworks/cn/linux/l-pow-oprofile/

使用 GNU profiler 来提高代码运行速度相关推荐

  1. python函数可以提高代码执行速度吗_Python代码运行速度慢?这五种方法很管用

    对于Python很多人还是比较了解的,虽然说Python有很多优势但同样具有劣势,Python最大的劣势就是运行效率慢,那么如何提高Python代码运行速度呢?这五种方法很管用. 1.PyPy:在选择 ...

  2. iar代码优化影响运行速度吗_Java 性能优化:教你提高代码运行的效率

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:五月的仓颉 cnblogs.com/xrq730/p/486 ...

  3. 分享9个提高代码运行效率的小技巧

    学习之余看到的一片文章,转载出来,经常看看,好处多多! 我们写程序的目的就是使它在任何情况下都可以稳定工作.一个运行的很快但是结果错误的程序并没有任何用处.在程序开发和优化的过程中,我们必须考虑代码使 ...

  4. 程序人生:提高代码运行效率的9个技巧

    我们写程序的目的,就是使它在任何情况下都可以稳定工作.一个运行的很快但结果错误的程序,并没有任何用处.在程序开发和优化的过程中,我们需要考虑代码使用的方式,以及影响它的关键因素.通常,我们要在程序的简 ...

  5. VBA-加快你的代码运行速度--转自ExcelHome

    原文链接:加快你的代码运行速度-Excel VBA程序开发-ExcelHome技术论坛 - 致敬,感恩,ExcelHome,在那里学到了很多 主题:VBA-加快你的代码运行速度​​​​​​​ 我们知道 ...

  6. python运行时很卡-Python代码运行速度慢?这五种方法很管用

    对于Python很多人还是比较了解的,虽然说Python有很多优势但同样具有劣势,Python最大的劣势就是运行效率慢,那么如何提高Python代码运行速度呢?这五种方法很管用. 1.PyPy:在选择 ...

  7. OpenCV学习系列教程第五篇:测试和提高代码的效率

    Opencv-Python学习系列教程第五篇 来自opencv-python官方学习文档,本人谨做翻译和注释,以及一些自己的理解 本文由作者翻译并进行代码验证,转载请注明出处~ 官方文档请参阅:htt ...

  8. Java 性能优化:教你提高代码运行的效率

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 作者:五月的仓颉 cnblogs.com/xrq730/p/486 ...

  9. 阿里程序员工作小技巧:理解CPU分支预测,提高代码效率

    技术传播的价值,不仅仅体现在通过商业化产品和开源项目来缩短我们构建应用的路径,加速业务的上线速率,体现也会在优秀程序员在工作效率提升,产品性能优化和用户体验改善等小技巧方面的分享,以提高我们的工作能力 ...

最新文章

  1. 三十六、请求分页管理方式
  2. ahjesus Ubuntu配置svn服务器
  3. 二十四、数据挖掘时序模式
  4. Step by step通过online方式做product archiving
  5. Servlet第二篇【Servlet调用图、Servlet细节、ServletConfig、ServletContext】
  6. 如何从JavaScript数组中获取多个随机唯一元素?
  7. 21天Jenkins打卡Day6安装插件
  8. python 中 * 的使用和表示含义
  9. Python实现数字转变为Excel的列
  10. HTML学习的第三天
  11. oracle怎么从右边截取,Oracle 字符串截取 从后向前截取方法
  12. linux系统uuid 一样,linux系统product_uuid和product_serial有何区别
  13. 苹果支付(内购项目)回调验证
  14. vxe-table可编辑表格使用下拉框时下拉框层级问题
  15. Android 实现HTTPS自签名证书(非常详细)
  16. 转手动档汽车的换挡技巧
  17. Streamlit(五) widgets-button
  18. 天猫仿站秒杀系统开发
  19. 计算机配色故障,计算机配色不理想?究竟问题出在哪?数据库,很重要!
  20. 海云安:违法移动应用增多,加强自身安全合规建设是关键

热门文章

  1. php截取剩余部分,PHP从字串中截取一部分,支持使用(*)模糊截取
  2. python中调用函数时必须有实参_Python之函数(一)定义函数以及传参
  3. linux c指定相对路径,linux c编程,选用popen()得到一个相对路径的绝对路径
  4. mfc 内嵌 本地html,MFC程序中内嵌网页,附示例工程
  5. oracle部署在mysql_Oracle数据库部署实施流程
  6. matlab if语句多个执行举例,初学Matlab,有两个语句,if语句和switch语句,有两个例子哪位大神能帮我讲讲...
  7. JetBrains遭美国调查,称其是被大规模黑客攻击的源头?
  8. 用了那么多年MySQL不知道Explain?怪不得性能那么差!
  9. Spring Cloud Config采用数据库存储配置内容
  10. SpringBoot整合security的登录问题