linux下程序时间测量方法

程序遇到瓶颈时,无论是CPU、网络、内存还是磁盘I/O瓶颈,确定瓶颈产生的模块、函数都是首先要解决的问题。程序各个模块运行的时间可以一定程度上反映出程序的瓶颈所在,下面简单地总结几种linux常用的程序时间测量方法:

 

1. linux下time命令的使用

 Linux下time命令可以获取到一个程序的执行时间,包括程序的实际运行时间(real time),以及程序运行在用户态的时间(user time)和内核态的时间(sys time)。

  使用方法:它的使用方法和前面讲过的strace类似,在待执行的命令前加上time即可。

   [leconte@localhost test]$ time ./myprogram

  real 0m0.020s

  user 0m0.000s

  sys 0m0.018s

 

  结果表明,程序实际运行时间0.020s,用户态运行时间接近0s,内核态运行时间0.018s。这是因为我们主要操作是使用文件相关的系统调用,程序大部分时间工作在内核态。

  需要注意的是,real并不等于user+sys的总和。real代表的是程序从开始到结束的全部时间,即使程序不占CPU也统计时间。而user+sys是程序占用CPU的总时间,因此real总是大于或者等于user+sys的。

 

  使用目的:通过此命令可以得到程序运行的时间,确定用户态与内核态的运行比例.

 

2. 使用gettimeofday系统函数

通过在程序的不同的位置记录系统时间,通过不同位置的时间差值来计算程序的不同的资源消耗。函数信息如下:

#include <sys/time.h>

     #include <time.h>

   函数原型:int gettimeofday(struct timeval *tv, struct timezone *tz);

       struct timeval {

               time_t         tv_sec;        /* seconds */

               suseconds_t    tv_usec; /* microseconds */

       };

       struct timezone {

               int tz_minuteswest; /* minutes W of Greenwich */

               int tz_dsttime;     /* type of dst correction */

       };

 

3.使用gprof和oprofile工具

gprof是GNU工具之一,它在编译的时候在每个函数的出入口加入了profiling的代码,运行时统计程序在用户态的执行信息,可以得到每个函数的调用次数,执行时间,调用关系等信息,简单易懂。适合于查找用户级程序的性能瓶颈,对于很多时间都在内核态执行的程序,gprof不适合。

oprofile也是一个开源的profiling工具,它使用硬件调试寄存器来统计信息,进行profiling的开销比较小,而且可以对内核进行profiling。它统计的信息非常的多,可以得到cache的缺失率,memory的访存信息,分支预测错误率等等,这些信息gprof是得不到的,但是对于函数调用次数,它是不能够得到的。

    简单来说,gprof简单,适合于查找用户级程序的瓶颈,而oprofile稍显复杂,但是得到的信息更多,更适合调试系统软件。

 

gprof Quick Start

gprof是gnu binutils工具之一,默认情况下linux系统当中都带有这个工具。

使用 -pg 选项来编译hello.c,如果要得到带注释的源码清单,则需要增加 -g 选项。运行: gcc -pg -g -o hello hello.c

运行应用程序: ./hello 会在当前目录下产生gmon.out文件

使用gprof来分析gmon.out文件,需要把它和产生它的应用程序关联起来:

gprof hello gmon.out -p 得到每个函数占用的执行时间

gprof hello gmon.out -q 得到call graph,包含了每个函数的调用关系,调用次数,执行时间等信息。

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。

opcontrol --init 加载模块,mout /dev/oprofile 创建必需的文件和目录

opcontrol --no-vmlinux 或者 opcontrol --vmlinux=/boot/vmlinux-`uname -r` 决定是否对kernel进行profiling

opcontrol --reset 清楚当前会话中的数据

opcontrol --start 开始profiling

./hello 运行应用程序,oprofile会对它进行profiling

opcontrol --dump 把收集到的数据写入文件

opcontrol --stop 停止profiling

opcotrol -h 关闭守护进程oprofiled

opcontrol --shutdown 停止oprofiled

opcontrol --deinit 卸载模块

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

opreport使用 http://oprofile.sourceforge.net/doc/opreport.html

opannotate使用 http://oprofile.sourceforge.net/doc/opannotate.html

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

 

4. 使用truss、strace或ltrace诊断软件

truss 和strace用来跟踪一个进程的系统调用或信号产生的情况,而 ltrace用来跟踪进程调用库函数的情况。truss是早期为System V R4开发的调试程序,包括Aix、FreeBSD在内的大部分Unix系统都自带了这个工具;而strace最初是为SunOS系统编写的,ltrace 最早出现在GNU/Debian Linux中。这两个工具现在也已被移植到了大部分Unix系统中,大多数Linux发行版都自带了strace和ltrace,而FreeBSD也可通过Ports安装它们。 

     你不仅可以从命令行调试一个新开始的程序,也可以把truss、strace或ltrace绑定到一个已有的PID上来调试一个正在运行的程序。三个调试工具的基本使用方法大体相同,下面仅介绍三者共有,而且是最常用的三个命令行参数: 

-f :除了跟踪当前进程外,还跟踪其子进程。 

-o file :将输出信息写到文件file中,而不是显示到标准错误输出(stderr)。 

-p pid :绑定到一个由pid对应的正在运行的进程。此参数常用来调试后台进程。 

 

-r 打印出相对时间关于,,每一个系统调用. 

-t 在输出中的每一行前加上时间信息. 

-tt 在输出中的每一行前加上时间信息,微秒级. 

-ttt 微秒级输出,以秒了表示时间. 

-T 显示每一调用所耗的时间. 

 

5. 间隔计数

       操作系统用计时器来记录每个进程使用的累计时间,原理很简单,计时器中断发生时,操作系统会在当前进程列表中寻找哪个进程是活动的,一旦发现进程A正在运行立马就给进程A的计数值增加计时器的时间间隔(这也是引起较大误差的原因)。当然不是统一增加的,还要确定这个进程是在用户空间活动还是在内核空间活动,如果是用户模式,就增加用户时间,如果是内核模式,就增加系统时间。这种方法的原理虽然简单但不精确。如果一个进程的运行时间很短,短到和系统的计时器间隔一个数量级,用这种方法测出来的结果必然是不够精确的,头尾都有误差。不过,如果程序的时间足够长,这种误差有时能够相互弥补,一些被高估一些被低估,平均下来刚好。从理论上很难分析这个误差的值,所以一般只有程序达到秒的数量级时用这种方法测试程序时间才有意义。

       这种方法最大的优点是它的准确性不是非常依赖于系统负载。

       实现方法之一就是上面介绍的time命令,之二是使用tms结构体和times函数。

       在Linux中,提供了一个times函数,原型是

       clock_t times( struct tms * buf );

这个tms的结构体为

struct tms

{

       clock_t tms_utime;             //user time

       clock_t tms_stime;             //system time

       clock_t tms_cutime;    //user time of reaped children

       clock_t tms_cstime;     //system time of reaped children

}

这里的cutime和cstime,都是对已经终止并回收的时间的累计,也就是说,times不能监视任何正在进行中的子进程所使用的时间。使用times函数需要包含头文件sys/times.h。

 

6. 周期计数

       为了给计时测量提供更高的准确度,很多处理器还包含一个运行在始终周期级别的计时器,它是一个特殊的寄存器,每个时钟周期它都会自动加1。这个周期计数器呢,是一个64位无符号数,直观理解,就是如果你的处理器是1GHz的,那么需要570年,它才会从2的64次方绕回到0,所以你大可不必考虑溢出的问题。但是这种方法是依赖于硬件的。首先,并不是每种处理器都有这样的寄存器的;其次,即使大多数都有,实现机制也不一样,因此,我们无法用统一的,与平台无关的接口来使用它们。这下,就要使用汇编了。当然,在这里实际用的是C语言的嵌入汇编:

 

void counter( unsigned *hi, unsigned *lo )

{

asm(”rdtsc; movl %%edx,%0; movl %%eax, %1″

: “=r” (*hi), “=r” (*lo)

:

: “%edx”, “%eax”);

}

第一行的指令负责读取周期计数器,后面的指令表示将其转移到指定地点或寄存器。这样,我们将这段代码封装到函数中,就可以在需要测量的代码前后均加上这个函数即可。最后得到的hi和lo值都是两个,除了相减得到间隔值外,还要进行一些处理,在此不表。

       不得不提出的是,周期计数方式还有一个问题,就是我们得到了两次调用counter之间总的周期数,但我们不知道是哪个进程使用了这些周期,或者说处理器是在内核还是在用户模式中。间隔计数的好处就是它是操作系统控制给进程计时的,我们可以知道具体哪个进程呢个模式;但是周期计数只测量经过的时间,他不管是哪个进程使用的。所以,用周期计数的话必须很小心。举个例子:

       double time()

       {

              start_counter();

              p();

              get_counter();

}

这样一段程序,如果机器的负载很重,会导致p运行时间很长,而其实p函数本身是不需要运行这么长时间的,而是上下文切换等过程将它的时间拖长了。

       而且,转移预测和高速缓存的命中率,对这个计数值也会有影响。通常情况下,为了减少高速缓存不命中给我们程序执行时间带来的影响,可以执行这样的代码:

       double time_warm(void)

       {

              p();

              start_counter();

              p();

              get_counter();

}

它让指令高速缓存和数据高速缓存都得到了warm-up。

       接下来又有问题。如果我们的应用是属于那种每次执行都希望访问新的数据的那种呢?在这种情况下,我们希望让指令高速缓存warm-up,而数据高速缓存不能warm-up,很明显,time-warm函数低估我们的运行时间了。进一步修改:

       double time_cold( void )

       {

              p();

              clear_cache();

              start_counter();

              p();

              get_counter();

}

注意,程序中加入了一个清除数据缓存的函数,这个函数的具体实现很简单,依情况而定,比如举个例子:

       volatile int tmp;

       static int dummy[N];     //N是需要清理缓存的字节数

 

       void clear_cache( void )

       {

              int i, sum = 0;

              for( i=1; i<N; i++)

                     dummy[i] = 2;

              for( i=1; i<N; i++)

                     sum += dummy[i];

              tmp = sum;

}

具体原理很简单,定义一个数组并在其上执行一个计算,计算过程中的数据会覆盖高速数据缓存中原有的数据。每一次的store和load都会让高速数据缓存cache这个数组,而定义为volatile的tmp则保证这段代码不会被优化。

       这样做,是不是就万无一失了呢?不是的,因为大多数处理器,L2高速缓存是不分指令和数据的,这样clear_cache会让所有p的指令也被清除,只不过:L1缓存中的指令还会保留而已。

       其实上面提到的诸多原因,都是我们不能控制的,我们无法控制让高速缓存去加载什么,不去加载什么,加载时去掉什么。保留什么。而且,这些误差通常都是会过高估计真实的运行时间。那么具体使用时,有没有什么办法来改善这种情况呢?有,就是The K-Best Measurement Scheme。这其实很麻烦,所以在具体实践中都不用它。

 

7. clock函数

       clock也是一个库函数,仍然包含在time.h中,函数原型是:

       clock_t clock( void );

功能:返回自程序开始运行的处理器时间,如果无可用信息,返回-1。转换返回值若以秒计需除以CLOCKS_PER_SECOND。(注:如果编译器是POSIX兼容的,CLOCKS_PER_SECOND定义为1000000。)[5]

       使用clock函数也比较简单:在要计时程序段前后分别调用clock函数,用后一次的返回值减去前一次的返回值就得到运行的处理器时间,然后再转换为秒。举例如下:

       clock_t starttime, endtime;

       double totaltime;

       starttime = clock();

       …

       endtime = clock();

       totaltime = (double)( (endtime - starttime)/(double)CLOCKS_PER_SEC );

 

网络资源

gprof 用户手册 http://sourceware.org/binutils/docs-2.17/gprof/index.html

oprofile官方站点 http://oprofile.sourceforge.net/

使用 GNU profiler 来提高代码运行速度 http://www-128.ibm.com/developerworks/cn/linux/l-gnuprof.html

使用 OProfile for Linux on POWER 识别性能瓶颈 http://www-128.ibm.com/developerworks/cn/linux/l-pow-oprofile/

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值