一. 安装与简介
从主页http://code.google.com/p/google-perftools/downloads/list下载源码包,解压后使用命令序列./configure;make;make install安装。如果make报错,请使用./configure --enable-frame-pointers从新来过。默认安装路径为/usr/local/,头文件和库文件分别在/usr/local/inlcude/google/和/usr/local/lib/下。需要设置环境变量export LD_LIBRARAY_PATH=/usr/local/lib。跟valgrind的profiler工具的不同之处是,Google perftools使用在源程序中插入profiler代码的方式,而不是valgrind的虚拟机方式,所以Google perftools以库文件的形式提供了一系列函数接口。为了使用图形化结果还需要安装gv,可使用apt-get安装:sudo apt-get install gv。
Google Perftools包括三个工具(注:包括tcmalloc应该是4个),三个工具均支持多线程程序,以下分别介绍。
一. 安装与简介
从主页http://code.google.com/p/google-perftools/downloads/list下载源码包,解压后使用命令序列./configure;make;make install安装。如果make报错,请使用./configure --enable-frame-pointers从新来过。默认安装路径为/usr/local/,头文件和库文件分别在/usr/local/inlcude/google/和/usr/local/lib/下。需要设置环境变量export LD_LIBRARAY_PATH=/usr/local/lib。跟valgrind的profiler工具的不同之处是,Google perftools使用在源程序中插入profiler代码的方式,而不是valgrind的虚拟机方式,所以Google perftools以库文件的形式提供了一系列函数接口。为了使用图形化结果还需要安装gv,可使用apt-get安装:sudo apt-get install gv。
Google Perftools包括三个工具,三个工具均支持多线程程序,以下分别介绍。
二. CPU profiler
通过cpu中断采样的方式来统计每个函数被采样的次数,占总采样次数的百分比,调用的子函数的被采样次数等。通过这些信息来找到程序的cpu性能瓶颈,从而有针对性的进行优化。
在要检测的程序的源文件中包含头文件google/profiler.h(默认全路径为/usr/local/include),并在需要统计的代码段前后调用函数ProfilerStop(char*)和ProfilerStop(),其中ProfilerStart的参数是输出profile的文件路径。注意,可以对多个代码段作profile,但是如果使用同样的输出文件,后一次 profile的输出会完全覆盖前一次的输出。然后编译源程序并使用-lprofiler连接选项得到链接了cpu profiler代码的可执行程序,这里推荐使用-O0编译选项,否则可能无法得到函数调用关系图。以后每次运行可执行程序,都会生成 profile输出文件,输出文件是人不可识别的二进制文件。CPU profiler采样的频率是可调的,设置环境变量CPUPROFILE_FREQUENCY来确定每秒钟的采样次数,默认为100。
输出的profile文件可以用工具pprof(一个perl程序)来方便的生成各种人可读的形式。下面以一个随机生成1000000个整数插入一个红黑树中的程序来演示ppro的使用,可执行程序名为RBtree,生成的profile文件名为RBTree.prof,作profile的代码段主要调用了RBTree::insert,RBtree::insertFixup和rand等函数。
pprof的使用形式都是: pprof --option [ --focus=<regexp> ] [ --ignore=<regexp> ] [--line or addresses or functions] 可执行文件路径 对应的profile路径。方括号中的项目是可选项目。<regexp>表示正则表达式。
option可取的值有:text,gv,dot,ps,pdf,gif,list=<regexp>,disasm=<regexp>。表示不同的输出形式。其中list=<regexp>表示输出匹配正则表达式的函数的源代码,diasm=<regexp>表示输出匹配正则表达式的函数的反汇编代码。text是字符统计输出形式,其它的对应不同的图形文件格式。
--focus=<regexp>表示只统计函数名匹配正则表达式的函数的采样,--ignore=<regexp>表示不统计函数名匹配正则表达式的函数的采样。
[--line or addresses or functions]表示生成的统计是基于代码行,指令地址还是函数的,默认是函数。
pprof --text ./RBtree ./RBtree.prof 生成的字符统计结果如下。
501 62.2% 62.2% 714 88.6% RBTree::insert
84 10.4% 72.6% 84 10.4% RBTree::defaultCmp
80 9.9% 82.5% 154 19.1% RBTree::nodeCmp
61 7.6% 90.1% 73 9.1% RBTree::insertFixup
47 5.8% 95.9% 47 5.8% malloc_trim
9 1.1% 97.0% 746 92.6% main
6 0.7% 97.8% 6 0.7% RBTree::rightRotate
6 0.7% 98.5% 6 0.7% RBTree::leftRotate
5 0.6% 99.1% 5 0.6% malloc
3 0.4% 99.5% 3 0.4% operator new
3 0.4% 99.9% 3 0.4% random_r
1 0.1% 100.0% 1 0.1% rand
0 0.0% 100.0% 755 93.7% __libc_start_main
每行对应一个函数的统计。第一,二列是该函数的本地采样(不包括被该函数调用的函数中的采样次数)次数和比例,第三列是该函数本地采样次数占当前所有已统计函数的采样次数之和的比例。第四,五列是该函数的累计采样次数(包括其调用的函数中的采样次数)和比例。
pprof --gif ./RBtree ./RBtree.prof > graph.gif 生成的gif统计图如下。
图中每个节点对应一个函数,节点中的文字分别为类名,函数明,本地采样次数比例和累计采样次数比例(如果跟本地相同则省略)。每条边表示一个函数调用关系:caller调用callee,边上的数字表示callee中因为caller调用而被采样的次数。
pprof如果不带任何选项调用(只有可执行文件路径和profile文件路径)则进入互动模式,在互动模式下可使用gv,gif,text等命令来替代前面介绍的带选项的pprof调用。
二. Heap Checker
堆内存泄漏检测工具。使用简单,先在链接被检查程序的时候用-ltcmalloc选项连接Goolge Perftools的堆内存管理库tcmalloc(tcmalloc会替代C的堆内存管理库),然后每次用命令行“env HEAPCHECK=normal 可执行程序路径”来进行检查,其中检查形式normal可以替换成其他值,检查的结果会以屏幕报告的形式给出。以下给出一个实例:
# cat test_heap_checker.cpp
#include <cstdio>
#include <cstdlib>
int* fun(int n)
{
int *p1=new int[n];
int *p2=new int[n];
return p2;
}
int main()
{
int n;
scanf("%d",&n);
int *p=fun(n);
delete [] p;
return 0;
}
# g++ -O0 -g test_heap_checker.cpp -ltcmalloc -o test_heap_checker
# env HEAPCHECK=normal /home/hongcheng/mycode/google-perftools-tests/test_heap_checker
WARNING: Perftools heap leak checker is active -- Performance may suffer
100
Have memory regions w/o callers: might report false leaks
Leak check _main_ detected leaks of 400 bytes in 1 objects
The 1 largest leaks:
Leak of 400 bytes in 1 objects allocated from:
If the preceding stack traces are not enough to find the leaks, try running THIS shell command:
pprof /home/hongcheng/mycode/google-perftools-tests/test_heap_checker "/tmp/test_heap_checker.13379._main_-end.heap" --inuse_objects --lines --heapcheck --edgefraction=1e-10 --nodefraction=1e-10 --gv
If you are still puzzled about why the leaks are there, try rerunning this program with HEAP_CHECK_TEST_POINTER_ALIGNMENT=1 and/or with HEAP_CHECK_MAX_POINTER_OFFSET=-1
Exiting with error code (instead of crashing) because of whole-program memory leaks
上面的报告显示有400个字节的内存泄漏,并提示使用pprof进一步跟踪泄漏来源的方法。
包括normal在内总共有4种泄漏检查方式:minimal,忽略进入main函数之前的初始化过程;normal,报告所有的无法再引用的内存对象;strick,在normal的基础上增加一些额外的检查;draconian,在程序退出的时候存在未释放的内存的情况下报错。
除了前面使用env命令行的全局内存泄漏检查方式外,还可以作对代码段的更加细粒度的泄漏检查。这里需要先在源代码中包含头文件google/heap-checker.h。下面是一个检查代码段的实例:
HeapLeakChecker heap_checker("test_foo");
{
code that exercises some foo functionality;
this code should preserve memory allocation state;
}
if (!heap_checker.SameHeap()) assert(NULL == "heap memory leak");
在进入代码段之前建立当前堆内存使用情况的snapshot,然后在结束代码段的时候通过与记录的snapshot对比检查是否有泄漏。方法NoLeaks()也可以用在这里。下面是一个实例:
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <google/heap-checker.h>
int* fun(int n)
{
int *p2;
HeapLeakChecker heap_checker("fun");
{
new int[n];
p2=new int[n];
//delete [] p1;
}
assert(!heap_checker.NoLeaks());
return p2;
}
int main(int argc,char* argv[])
{
int n;
scanf("%d",&n);
int *p=fun(n);
delete [] p;
return 0;
}
注意被检查程序的main函数形式必须为带2个参数的形式,否则会在编译时报告重复定义。运行env命令行将会报告assert失败。
另外,还可以跳过某些代码段的检查,方式如下:
{
HeapLeakChecker::Disabler disabler;
<leaky code>
}
<leaky code>处的代码将被heap-checker忽略。
三. Heap Profiler
堆内存使用情况统计工具。有两种使用方法。
1. 全局profile,在连接的时候使用-ltcmalloc选项,然后使用命令行“env HEAPPROFILE=prefix 可执行程序路径”来生成若干profile数据文件,这里的prefix是生成的profile文件的路径前缀。
2. 代码段profile,在源程序中包含头文件<google/heap-profiler.h>,然后调HeapProfilerStart(char* prefix),HeapProfilerStop(),
HeapProfilerDump(char* filename)和GetHeapProfile()函数来确定代码段,输出profile文件等。其中HeapProfilerStart的参数是周期性输出文件的前缀,HeapProfilerDump的参数是当前heap的profile输出文路径。
两种方式都会周期性生成 prefix.0000.heap, prefix.0001.heap一系列profile文件,用于进一步的分析。这里的周期性是指一旦一定数量的对内存被allocated就生成新的profile文件。环境变量HEAP_PROFILE_ALLOCATION_INTERVAL用于控制这一数量(单位为字节),默认为1GB。
可以用pprof程序处理生成的profile数据文件以身生成可读的文本或者图形统计文件,使用方法跟前面介绍的CPU profiler中的pprof几乎一样,除了采样次数被替换成申请的堆内存数量。
性能分析:
1、功能简介
它的主要功能就是通过采样的方式,给程序中cpu的使用情况进行“画像”,通过它所输出的结果,我们可以对程序中各个函数(得到函数之间的调用关系)耗时情况一目了然。在对程序做性能优化的时候,这个是很重要的,先把最耗时的若干个操作优化好,程序的整体性能提升应该十分明显,这也是做性能优化的一个最为基本的原则—先优化最耗时的。
2、安装
1、下载gperftools
Wget https://code.google.com/p/gperftools/downloads/detail?name=gperftools-2.0.tar.gz
2、tar –xzf gperftools-2.0.tar.gz
3、cd gperftools-2.0
4、./configure --prefix=/usr/local –enable-frame-pointers
5、make && make install
ps:编译时打开了 –enable-frame-pointers ,这要求被测试的程序在编译时要加上gcc编译选项,否则某些多线程程序可能会 core:
CCFLAGS=-fno-omit-frame-pointer
ps:perftools对2.4内核的多线程支持不是很好,只能分析主线程,但是2.6内核解决了这个问题。
安装图形化分析工具kcachegrind:
kcachegrind用来分析产生的profiling文件,linux环境下使用。
kcachegrind install:sudo apt-get install kcachegrind
3、使用
方法有三种:
1、直接调用提供的api:这种方式比较适用于对于程序的某个局部来做分析的情况,直接在要做分析的局部调用相关的api即可。
方式:调用函数:ProfilerStart()
and ProfilerStop()
2、链接静态库:这种方式是最为常用的方式,后面会有详细的介绍。
方式:在代码link过程中添加参数 –lprofiler
For example:gcc […] -o helloworld –lprofiler
运行程序:env CPUPROFILE=./helloworld.prof ./helloworld
指定要profile的程序为helloworld,并且指定产生的分析结果文件的路径为./helloworld.prof
3、链接动态库:这种方式和静态库的方式差不多,但通常不推荐使用,除非使用者不想额外链一个静态库(因为链接静态库会增大binary的大小)的情况,可以考虑使用这种方式。
方式:运行时使用LD_PRELOAD,e.g. % env LD_PRELOAD="/usr/lib/libprofiler.so" <binary>
(不推荐这种方式)。
Ps
:env是linux下插入环境变量的shell命令
4、 查看收集数据结果
查看profile结果:pprof工具,它是一个perl的脚本,通过这个工具,可以将google-perftool的输出结果分析得更为直观,输出为图片、pdf等格式。
Ps:在使用pprof之前需要先安装运行per15,如果要进行图标输出则需要安装dot,如果需要--gv模式的输出则需要安装gv。
调用pprof分析数据文件:
% pprof /bin/ls ls.prof
Enters "interactive" mode
% pprof --text /bin/ls ls.prof
Outputs one line per procedure
% pprof --gv /bin/ls ls.prof
Displays annotated call-graph via 'gv'
% pprof --gv --focus=Mutex /bin/ls ls.prof
Restricts to code paths including a .*Mutex.* entry
% pprof --gv --focus=Mutex --ignore=string /bin/ls ls.prof
Code paths including Mutex but not string
% pprof --list=getdir /bin/ls ls.prof
(Per-line) annotated source listing for getdir()
% pprof --disasm=getdir /bin/ls ls.prof
(Per-PC) annotated disassembly for getdir()
% pprof --text localhost:1234
Outputs one line per procedure for localhost:1234
% pprof --callgrind /bin/ls ls.prof
Outputs the call information in callgrind format
分析callgrind的输出:
使用kcachegrind工具来对.callgrind输出进行分析
e.g. % pprof --callgrind /bin/ls ls.prof > ls.callgrind
% kcachegrind ls.callgrind
4、举例
事例一:cpu_profiler_example.cpp,在代码中插入标签,可以针对某个函数进行特定的profile
代码如下:
关注两个函数:ProfilerStart()
and ProfilerStop()
Makefile:
-L 动态链接库地址,但是有可能程序执行的时候,找不到动态链接库,所以得
export LD_LIBRARY_PATH=LD_LIBRARY_PATH:"/home/work/alex/tools/gperftools/lib"
1)执行./cpu_profile_example
生成一个性能数据文件: cpu_profiler_example_29502.prof
Ps:当然指定性能数据文件生成的路径和文件名:
CPUPROFILE=/tmp/profile ./myprogram
将在/tmp目录下产生profile性能数据文件
2)分析性能数据
pprof -text cpu_profiler_example cpu_profiler_example_3875.prof
Text输出结果分析:
14 2.1% 17.2% 58 8.7% std::_Rb_tree::find
含义如下:
14:find函数花费了14个profiling samples
2.1%:find函数花费的profiling samples占总的profiling samples的比例
17.2%:到find函数为止,已经运行的函数占总的profiling samples的比例
58:find函数加上find函数里的被调用者总共花费的profiling samples
8.7%:find函数加上find函数里的被调用者总共花费的profiling samples占总的profiling samples的比例
std::_Rb_tree::find:表示profile的函数
ps: 100 samples a second,所以得出的结果除以100,得秒单位
Ldd可以查看一个程序要链接那些动态库:
事例二:cpu_profiler_example.cpp,不需要在代码里添加任何标签,将profile所有的函数。
代码如下:
Makefile:
1)执行程序,生成性能数据文件
CPUPROFILE=/tmp/profile ./cpu_profiler_example
2)分析数据文件
1)pprof -text cpu_profiler_example profile
2)命令行交互模式
事例三:由于我们的程序有可能是服务程序,而服务程序不会自动执行完退出,如果以ctrl+c退出也不是正常的exit(0)的方式退出,而这会导致我们在profile的时候,收集到的数据不全甚至是空的,采用如下解决办法:
将ProfilerStart和ProfilerStop这2个函数封装到两个信号处理函数中,给服务程序发信号SIGUSR1,就开始profile,给服务程序发信号SIGUSR2,就停止profile。这样我们可以随时对程序进行profiling,并获得数据。
代码如下:
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 #include <signal.h>
5 #include <google/profiler.h>
6
7 //SIGUSR1: start profiling
8 //SIGUSR2: stop profiling
9
10 static void gprof_callback(int signum)
11 {
12 if (signum == SIGUSR1)
13 {
14 printf("Catch the signal ProfilerStart\n");
15 ProfilerStart("bs.prof");
16 }
17 else if (signum == SIGUSR2)
18 {
19 printf("Catch the signal ProfilerStop\n");
20 ProfilerStop();
21 }
22 }
23
24 static void setup_signal()
25 {
26 struct sigaction profstat;
27 profstat.sa_handler = gprof_callback;
28 profstat.sa_flags = 0;
29 sigemptyset(&profstat.sa_mask);
30 sigaddset(&profstat.sa_mask, SIGUSR1);
31 sigaddset(&profstat.sa_mask, SIGUSR2);
32
33 if ( sigaction(SIGUSR1, &profstat,NULL) < 0 )
34 {
35 fprintf(stderr, "Fail to connect signal SIGUSR1 with start profiling");
36 }
37 if ( sigaction(SIGUSR2, &profstat,NULL) < 0 )
38 {
39 fprintf(stderr, "Fail to connect signal SIGUSR2 with stop profiling");
40 }
41 }
42
43 int loopop_callee()
44 {
45 int n=0;
46 for(int i=0; i<10000; i++)
47 {
48 for(int j=0; j<10000; j++)
49 {
50 n |= i%100 + j/100;
51 }
52 }
53 return n;
54 }
55
56 int loopop()
57 {
58 int n=0;
59 while(1)
60 {
61 for(int i=0; i<10000; i++)
62 {
63 for(int j=0; j<10000; j++)
64 {
65 n |= i%100 + j/100;
66 }
67 }
68 printf("result: %d\n", (loopop_callee)() );
69 }
70 return n;
71 }
72
73 int main(int argc,char** argv)
74 {
75 char program[1024]={0};
76 //snprintf(program,1023,"%s_%d.prof",argv[0],getpid());
77 setup_signal();
78 printf("result: %d\n", (loopop)() );
79 return 0;
80 }
关注两个函数gprof_callback和setup_signal。
启动程序,可以采用kill -s SIGUSR1 5722和kill -s SIGUSR2 5722来开始采集和停止采集,5722是进程pid。
5、心得
最后,补充一点,要用google-perftool来分析程序,必须保证程序能正常退出。
采用kcachegrind查看函数之间依赖,并分析程序性能