Linux用户态程序计时方式详解
前言
良好的计时器可帮助程序开发人员确定程序的性能瓶颈,或对不同算法进行性能比较。但要精确测量程序的运行时间并不容易,因为进程切换、中断、共享的多用户、网络流量、高速缓存访问及转移预测等因素都会对程序计时产生影响。
本文将不考虑这些影响因素(相关资料可参考《深入理解计算机系统》一书),而仅仅关注Linux系统中用户态程序执行时间的计算方式。除本文所述计时方式外,还可借助外部工具统计耗时,如《Linux调试分析诊断利器——strace》一文中介绍的strace。
本文示例代码的运行环境如下:
一 基本概念
1.1 日历时间
Coordinated Universal Time(UTC):世界协调时间(又称世界标准时间),旧称格林威治标准时间(Greenwich Mean Time, GMT)。
Calendar Time:日历时间,即从一个标准时间点到此时的时间所经过的秒数。该标准时间点因编译器而异,但对编译系统而言标准时间点不变。该编译系统中的时间对应的日历时间都通过该标准时间点衡量,故日历时间是“相对时间”。UNIX/Linux的时间系统由“新纪元时间(Epoch)”开始算起,该起点指定为1970年1月1日凌晨0时0分0秒(格林威治时间)。Microsoft C/C++ 7.0中标准时间点指定为1899年12月31日0时0分0秒,而其它版本的Microsoft C/C++和所有不同版本的Visual C++中标准时间点指定为1970年1月1日0时0分0秒。日历时间与时区无关。
Epoch:时间点。时间点在标准C/C++中是一个整数(time_t),它用此刻的时间和标准时间点相差的秒数(即日历时间)来表示。目前大部分UNIX系统采用32位记录时间,正值表示为1970年以后,负值则表示1970年以前。可简单地估算出所能表达的时间范围:1970±((231-1)/3600/24/365)≈[1901,2038]年。为表示更久远的时间,某些编译器厂商引入64位甚至更长的整型数来保存日历时间。
1.2 进程时间
进程时间也称CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴嗒计算,通常使用三个进程时间值,即实际时间(Real)、用户CPU时间(User)和系统CPU时间(Sys)。
实际时间指实际流逝的时间;用户时间和系统时间指特定进程使用的CPU时间。具体区别如下:
- Real是从进程开始执行到完成所经历的挂钟(wall clock)时间,包括其他进程使用的时间片(time slice)和本进程耗费在阻塞(如等待I/O操作完成)上的时间。该时间对应秒表(stopwatch)直接测量。
- User是进程执行用户态代码(内核外)耗费的CPU时间,仅统计该进程执行时实际使用的CPU时间,而不计入其他进程使用的时间片和本进程阻塞的时间。
- Sys是该进程在内核态运行所耗费的CPU时间,即内核执行系统调用所使用的CPU时间。
CPU总时间(User+Sys)是CPU执行用户进程操作和内核(代表用户进程执行)系统调用所耗时间的总和,即该进程(包括其线程和子进程)所使用的实际CPU时间。若程序循环遍历数组,则增加用户CPU时间;若程序执行exec或fork等系统调用,则增加系统CPU时间。
在多核处理器机器上,若进程含有多个线程或通过fork调用创建子进程,则实际时间可能小于CPU总时间——因为不同线程或进程可并行执行,但其时间会计入主进程的CPU总时间。若程序在某段时间处于等待状态而并未执行,则实际时间可能大于CPU总时间。其数值关系总结如下:
- Real < CPU,表明进程为计算密集型(CPU bound),利用多核处理器的并行执行优势;
- Real ≈ CPU,表明进程为计算密集型(CPU bound),未并行执行;
- Real > CPU,表明进程为I/O密集型(I/O bound),多核并行执行优势并不明显。
在单核处理器上,Real时间和CPU时间之差,即Real- (User + Sys)是所有延迟程序执行的因素的总和。可估算程序运行期间的CPU利用率为CpuUsage = (User + Sys)/ Real * 100(%)。
在SMP(对称多处理系统)上,该差值近似为Real* ProcessorNum - (User + Sys)。这些因素包括:
- 调入程序文本和数据的I/O操作;
- 获取程序实际使用内存的I/O操作;
- 由其它程序消耗的CPU用时;
- 由操作系统消耗的CPU用时。
二 计时方式
本节将基于下面的函数来讨论和对比各种计时方式:
1 #include <math.h> 2 #define TIME_LOOP_NUM 1000000*20 3 void TimingFunc(void){ 4 unsigned int i = 0; 5 double y = 0.0; 6 for(; i < TIME_LOOP_NUM; i++) 7 y = sin((double)i); 8 }
2.1 间隔计数
操作系统用计时器(timer)来记录每个进程使用的累计时间,该时间只是程序执行时间的粗略测量值。
操作系统维护着每个进程使用的用户时间量和系统时间量的计数值。当计时器中断发生时,操作系统会在当前进程列表中寻找活动的进程,并对该进程的计数值增加计时器时间间隔(通常10毫秒)。若该进程在内核模式中运行,则增加系统时间,否则增加用户时间。
这种间隔计数(“记账”)方法原理虽然简单但并不精确。若某进程运行时间很短(与系统计时器间隔相同数量级),且计时中断发生时发现进程正在运行,则不论进程已运行一段时间还是中断前1毫秒才开始运行,都会对计数器增加计时器时间间隔;中断发生时进程已切换的情况与之类似。因此,间隔计数时头尾都有误差。不过,若程序运行时间足够长(至少数秒),间隔计数的不准确性可能相互弥补(高估和低估的测量值平均后误差接近0)。理论上很难分析该误差值,故通常只有程序运行时间达到秒级时,采用间隔计数方法才有意义。此外,该方法的主要优点是其准确性不是非常依赖于系统负载。
Linux系统time命令和times库函数采用间隔计数方法测量命令或程序执行时间。
2.1.1 time命令
time命令可测量命令或脚本执行所耗时间及系统资源使用等信息,统计结果包含以下时间(以秒计):
- 实际执行时间(real time):从命令行执行到运行结束所消耗的时间;
- 用户CPU时间(user CPU time):命令在用户态中执行所消耗的CPU时间,即程序本身及其调用的库函数所使用的时间;
- 系统CPU时间(system CPU time):命令在内核态中执行所消耗的CPU时间,即由程序直接或间接调用的系统调用执行的时间。
Linux系统中,可使用Shell内置命令time,或GNU一般命令time(/usr/bin/time)来测试程序运行的时间。前者只负责计时,精度可达10毫秒;后者精度略低,但可访问getrusage系统调用的信息,并提供丰富的参数选项,包括指定输出文件等功能。
time命令不能用于测量程序内某个函数或某段代码的执行时间。
2.1.1.1 Shell命令
Shell内置命令time的使用格式为
time <command> [<arguments...>] |
命令行执行完成后,会在标准输出中打印执行该命令行的时间统计结果。例如:
可见Real>(User+Sys),说明处理器可能同时在执行其他进程,或本进程被阻塞或睡眠(sleep)。睡眠时间不计入用户时间和系统时间。阻塞可能是因为系统调用的错误使用,也可能是系统中的慢设备引起的。
又如统计在当前目录下查找文件hello.c所消耗的时间:
可见Real远大于(User+Sys),因为find命令遍历各个目录时进行大量磁盘I/O操作,这些操作比较耗时,因此大部分时间find进程都在等待磁盘I/O完成。此外,与文件相关的系统调用也会消耗系统时间。
再次运行find命令时,real时间将显著减小:
这得益于系统文件缓存,磁盘I/O操作次数显著减少。
以下两种方法可将time命令输出的时间信息重定向到文件里,如下所示:
{ time find . -name "hello.c"; } 2>hello.txt //代码块(花括号内侧空格符不可少) (time find . -name "hello.c") 2>hello.txt //子Shell(多占些资源)
注意上面示例中的花括号和小括号不可缺少,否则Shell会把time关键字后面的命令行作为一个整体进行处理,time命令本身的输出不会被重定向。内置命令time输出到标准错误,文件描述符2表示标准错误stderr。若还要包括find命令执行的结果,则可用:
(time find . -name "hello.c") 2>hello.txt 2>&1
2.1.1.2 GNU命令
GNU命令time的简单使用格式为
/usr/bin/time [options] <command> [<arguments...>] 或 \time [options] <command> [<arguments...>] |
命令执行完成后,输出与Shell内置命令time相似,但更详细。例如:
还可加上-v选项得到时间、内存和I/O等更具体的输出:
以下几种方法可将GNU工具time的输出信息重定向到文件里,如下所示:
1 /usr/bin/time --output=hello.txt find . -name "hello.c" 2 /usr/bin/time find . -name "hello.c" 2> hello.txt 3 \time --output=hello.txt find . -name "hello.c" 4 \time find . -name "hello.c" 2> hello.txt
若还要包括find命令执行的结果,则可用:
\time --output=hello.txt --append find . -name "hello.c" > hello.txt
若要控制输出时间的格式,可使用-f选项进行格式化(格式控制符用法见相关手册):
\time -f "\\t%E real,\\t%U user,\\t%S sys" find . -name "hello.c"
输出结果如下所示:
time命令的输出时间值中,用户时间和系统时间来自wait(2)或times(2)系统调用(依赖特定系统),实际时间由gettimeofday(2)中结束时间和起始时间相减得到。因为时间来源不同,故time命令对运行时间较短的任务计时时,会产生舍入错误(Rounding Errors),导致输出的时间精度仅为毫秒级(10毫秒)。
2.1.2 times函数
times是个GNU标准库函数,函数原型声明在sys/times.h头文件中:
clock_t times(struct tms *buf); |
该函数读取进程计时器,返回自系统启动以来(Linux 2.4及以前)或启动前(232/HZ)-300秒以来(Linux 2.6)经过的时钟滴嗒数(即挂钟时间)。Linux系统中,若参数buf为NULL指针,则时间值也通过返回值获取(POSIX未指定该行为,其他Unix系统实现多要求非空指针)。若执行失败,则函数返回(clock_t)-1。返回类型clock_t通常定义为长整型(long int)。tms结构体定义为:
1 struct tms{ 2 clock_t tms_utime; //user time 3 clock_t tms_stime; //system time 4 clock_t tms_cutime; //user time of reaped children 5 clock_t tms_cstime; //system time of reaped children 6 };
该结构体成员utime/stime含义与time命令输出相同,而cutime(用户CPU时间+子进程用户CPU时间)和cstime给出已经终止且被回收的子进程使用的累计时间。因此,times函数不能用于监视任何正在进行的子进程所使用的时间。此外,times函数返回相对时间,故其差值才有实用意义。
测量
某程序执行时间时,可在待计时程序段起始和结束处分别调用times函数,用后一次返回值减去前一次返回值得到运行该程序所消耗的时钟滴嗒数,再除以sysconf(_SC_CLK_TCK)转换为秒。如:
1 #include <sys/times.h> 2 void TimesTiming(void){ 3 clock_t tBeginTime = times(NULL); 4 TimingFunc(); 5 clock_t tEndTime = times(NULL); 6 double fCostTime = (double)(tEndTime - tBeginTime)/sysconf(_SC_CLK_TCK); 7 printf("[times]Cost Time = %fSec\n", fCostTime); 8 }
注意,库函数times与clock均获取CPU时间片数量,但计时单位不同,即sysconf(_SC_CLK_TCK)的值不一定等于CLOCKS_PER_SEC(通常前者为100,后者为1,000,000)——这可降低溢出的可能性。
sysconf(_SC_CLK_TCK)单位是次数每秒(或Hz),即每秒时钟滴嗒数。
2.2 周期计数rdtsc
从Intel Pentium开始,很多80x86微处理器都引入一个运行在时钟周期级别的时间戳计数寄存器TSC(Time Stamp Counter)。该寄存器以64位无符号整型数的格式,记录CPU上电以来所经过的时钟周期数,并在每个时钟信号(CLK,即处理器中用于接收外部振荡器的时钟信号输入引线)到来时加一。目前的处理器主频非常高,因此该寄存器可达到纳秒级的计时精度(在1GHz处理器上每个时钟周期为1纳秒)。
关于周期计时的最大长度,可用下列公式简单估算:
自CPU上电以来的秒数 = RDTSC读出的周期数 / CPU主频速率(Hz) |
若处理器主频为1GHz,则大约需要583~584年,才会从2的64次方(64位无符号整数所能表达的最大数字+1)绕回到0,所以大可不必考虑溢出问题。
通过机器指令RDTSC(Read Time Stamp Counter)可读取TSC时间戳值,并将其高32位存入EDX寄存器,低32位存入EAX寄存器。现有的C/C++编译器多数不直接支持使用RDTSC指令,需用内嵌汇编的方式访问。以下给出常见的几个RDTSC宏定义和封装函数:
1 #define RDTSC(low, high) asm volatile("rdtsc" : "=a" (low), "=d" (high)) 2 #define RDTSC_L(low) asm volatile("rdtsc" : "=a" (low) : : "edx") 3 #define RDTSC_LL(val) asm volatile("rdtsc" : "=A" (val)) 4 5 /* Set *hi and *lo to the high and low order bits of the cycle counter. 6 * Implementation requires assembly code to use the rdtsc instruction. */ 7 void AccessCounter(unsigned *hi, unsigned *lo){ 8 asm volatile("rdtsc; movl %%edx,%0; movl %%eax, %1" 9 : "=r" (*hi), "=r" (*lo) 10 : /* No input */ 11 : "%edx", "%eax"); 12 } 13 14 typedef unsigned long long cycle_t; 15 /* Record the current value of the cycle counter. */ 16 inline cycle_t CurrentCycle(void){ 17 cycle_t tRdtscRes; 18 asm volatile("rdtsc" : "=A" (tRdtscRes)); 19 return tRdtscRes; 20 } 21 inline cycle_t CurrentCycle2(void){ 22 unsigned hi, lo; 23 asm volatile ("rdtsc" : "=a"(lo), "=d"(hi)); 24 return ((cycle_t)lo) | (((cycle_t)hi)<<32); 25 }
其中,asm/volatile是GCC扩展的__asm__/__volatile__内嵌汇编关键字宏定义,若不考虑兼容性可直接采用不加下划线的格式。
通过TSC寄存器值可计算处理器主频,或测试处理器其他处理单元的运算速度。例如,一个周期计数相当于1/(处理器主频Hz数)秒,若处理器主频为1MHZ,则TSC值会在1秒内增加1000,000。在时间间隔1秒的前后分别记录TSC值,然后求差并除以1000,000,即可计算出以MHZ为单位的主频。代码如下:
1 #include <unistd.h> //alarm, pause 2 #include <sys/types.h> 3 #include <signal.h> //signal, kill 4 5 cycle_t tStart = 0, tEnd = 0; 6 void TimingHandler(int signo){ 7 tEnd = CurrentCycle(); 8 printf("CPU Frequency: %lldMHz\n", (tEnd-tStart)/1000000); 9 kill(getpid(), SIGINT); 10 } 11 12 void CalcCpuFreq(void){ 13 signal(SIGALRM, TimingHandler); 14 tStart = CurrentCycle(); 15 alarm(1); 16 while(1) 17 pause(); 18 }
考虑到sleep调用基于alarm和pause实现,可将上面的代码改造为更简单的方式:
1 unsigned gCpuFreqInHz = 0; //Record Cpu Frequency for later use 2 void CalcCpuFreq2(void){ 3 cycle_t tStart = CurrentCycle(); 4 sleep(1); //调用sleep时,进程挂起直到1秒睡眠时间到达。这期间经过的周期是被其他进程执行的。 5 cycle_t tEnd = CurrentCycle(); 6 gCpuFreqInHz = tEnd - tStart; 7 printf("CPU Frequency: %dMHz\n", gCpuFreqInHz/1000000); 8 }
执行输出CPU Frequency: 2696MHz(随每次执行可能稍有变化)。对比/proc文件系统中CPU信息(双核):
可见两者非常接近。
测量
某程序执行时间时,可在待计时程序段起始和结束处分别调用CurrentCycle函数(读取TSC值),用后一次的返回值减去前一次的返回值得到运行该程序所消耗的处理器时钟周期数,再除以处理器主频(Hz)转换为秒。如:
1 void RdtscTiming(void){ 2 cycle_t tStartCyc = CurrentCycle(); 3 TimingFunc(); 4 cycle_t tEndCyc = CurrentCycle(); 5 double fCostTime = (double)(tEndCyc-tStartCyc) /gCpuFreqInHz; 6 printf("[rdtsc]Cost Time = %fSec\n", fCostTime); 7 }
周期计数方式的优点是:
1) 高精度。在目前处理器上可获得纳秒级的计时精度。
2) 成本低。Pentium以上的i386处理器均支持RDTSC指令(其他平台也有类似指令),且访问开销极小。
其缺点是:
1) 周期计数指令因处理器平台和实现机制而异,没有与平台无关的统一访问接口,需借助内嵌汇编。
2) 因精度较高,故数据抖动比较厉害。RDTSC指令每次结果都不一样,经常有几百甚至上千的差距。
此外,周期计数方式只测量经过的时间,不关心哪个进程使用这些周期。机器负载、进程上下文切换、高速缓存命中率以及转移预测等都会影响计数值,导致过高估计程序的真实运行时间。《深入理解计算机系统》一书第9章中,深入讨论了这些因素对计时的影响以及尽可能获取精确计时的方法。
2.3 gettimeofday函数
gettimeofday是个库函数,函数原型声明在sys/time.h头文件中:
int gettimeofday(struct timeval *tv,struct timezone *tz); |
该函数查询系统时钟,并将当前时间存入tv所指结构体,当地时区信息存入tz所指结构体。其结构体定义为:
1 struct timeval{ 2 time_t tv_sec; //当前时间距UNIX时间基准的秒数 3 suseconds_t tv_usec; //一秒之内的微秒数,且1000000>tv_usec>=0 4 }; 5 struct timezone{ 6 int tz_minuteswest; //和Greenwich时间相差多少分钟 7 int tz_dsttime; //日光节约时间的状态 8 };
tv或tz均可为空,为空时不返回对应的结构体。通常只会获取当前时间,故置时区指针tz为空。
相对于间隔计数的小适用范围和周期计数的麻烦性,gettimeofday是一个可移植性更好相对较准确的方法。在Linux系统中,该函数计时精度可达到微秒级。
测量
某程序执行时间时,可在待计时程序段起始和结束处分别调用gettimeofday函数,用后一次获取的当前时间减去前一次获取的当前时间得到运行该程序所消耗的秒或微秒数。如:
1 #include <sys/time.h> 2 #define TIME_ELAPSED(codeToTime) do{ \ 3 struct timeval beginTime, endTime; \ 4 gettimeofday(&beginTime, NULL); \ 5 {codeToTime;} \ 6 gettimeofday(&endTime, NULL); \ 7 long secTime = endTime.tv_sec - beginTime.tv_sec; \ 8 long usecTime = endTime.tv_usec - beginTime.tv_usec; \ 9 printf("[%s(%d)]Elapsed Time: SecTime = %lds, UsecTime = %ldus!\n", __FUNCTION__, __LINE__, secTime, usecTime); \ 10 }while(0) 11 12 void GetTimeofDayTiming(void){ 13 struct timeval tBeginTime, tEndTime; 14 gettimeofday(&tBeginTime, NULL); 15 TimingFunc(); 16 gettimeofday(&tEndTime, NULL); 17 float fCostTime = 1000000*(tEndTime.tv_sec-tBeginTime.tv_sec)+ //先减后加避免溢出! 18 (tEndTime.tv_usec-tBeginTime.tv_usec); 19 fCostTime /= 1000000; 20 printf("[gettimeofday]Cost Time = %fSec\n", fCostTime); 21 }
使用gettimeofday函数计时时应注意:
1) 该函数的实现因系统和平台而异,故计时精度也随之而异。Linux系统直接提取硬件时钟来实现该函数,故精度接近周期计数精度;而Windows NT系统使用间隔计数实现,故精度较低。i386平台下采用内核sys_gettimeofday系统调用实现,调用时会向内核发送软中断,然后陷入内核态,内核进行软中断等处理并将执行结果复制到用户态,这些成本超过1微秒;而x86_64平台下采用vsyscall虚拟系统调用实现,创建一个用户态有权限访问的内核态共享内存页面,不通过中断即可获取系统时间,调用成本不到1微秒。
2) 该函数依赖于系统时间,若系统时间被人为改变则获取的时间随之改变。
3) 若计时过程中系统正在运行其他后台程序,可能会影响到最终的计时结果。
可用gettimeofday函数和usleep调用精确地计算处理器主频,如下:
1 void CalcCpuFreq3(void){ 2 struct timeval tStartTime, tEndTime; 3 4 cycle_t tStart = CurrentCycle(); 5 gettimeofday(&tStartTime, NULL); 6 usleep(1000000); //精度不高,由gettimeofday加以补偿 7 cycle_t tEnd = CurrentCycle(); 8 gettimeofday(&tEndTime, NULL); 9 10 int dwUsecDelay = 1000000 * (tEndTime.tv_sec - tStartTime.tv_sec) + 11 (tEndTime.tv_usec - tStartTime.tv_usec); 12 printf("CPU Frequency: %lldMHz\n", (tEnd-tStart)/dwUsecDelay); 13 }
2.4 clock函数
clock是ANSI C标准库函数,其函数原型声明在time.h头文件中:
clock_t clock(void); |
该函数返回自待测试程序进程开始运行起,到程序中调用clock函数时的处理器时钟计时单元数(俗称clock tick,即硬件时钟滴答次数)。若无法得到处理器时间,则返回-1。时钟计时单元的长短由CPU控制,但clock tick并非CPU时钟周期,而是一个C/C++基本计时单位。返回类型clock_t通常定义为有符号长整型(long int)。
使用clock函数时应注意以下几点:
1) 该函数返回处理器耗费在某程序上的时间(CPU时间片数量)。若程序中存在sleep函数,则sleep所消耗的时间将不计算在内,因为此时CPU资源被释放。
2) 返回值若以秒计需除以CLOCKS_PER_SEC宏,该宏表示一秒钟有多少个时钟计时单元(硬件滴答数),取值因系统而异。在POSIX兼容系统中,CLOCKS_PER_SEC值为1,000,000,即1MHz(此时返回值单位为微秒)。通过(231-1)/1000000/60≈35.8可估算出clock函数超过半小时后将会溢出。
3) 该函数仅能返回毫秒级的计时精度(大致与操作系统的线程切换时间相当),低于精度的程序计为0毫秒。因此,该函数适用于测量一些耗时较长(大于10ms)的大型程序或循环程序。
4) 当程序单线程或单核心机器运行时,该函数计时准确;但多线程环境下并发执行时不可使用,因为结束时间与起始时间之差是多个核心总共执行的时钟滴答数,会造成计时偏大。
5) 该函数未考虑CPU被子进程使用的情况,也不能区分用户模式和内核模式。该函数计量进程占用的CPU时间,大约是用户时间和系统时间的总和。
测量
某程序执行时间时,可在待计时程序段起始和结束处分别调用clock函数,用后一次的返回值减去前一次的返回值得到运行该程序所消耗的处理器时钟计时单元数,再除以CLOCKS_PER_SEC转换为秒。如:
1 #include <time.h> 2 void ClockTiming(void){ //可尝试在计时间隔内调用sleep(5),观察计时结果是否增加5秒 3 clock_t tBeginTime = clock(); //记录起始时间 4 TimingFunc(); //待计时函数 5 clock_t tEndTime = clock(); //记录结束时间 6 double fCostTime = (double)(tEndTime - tBeginTime)/CLOCKS_PER_SEC; //注意类型强制转换 7 printf("[clock]Cost Time = %fSec\n", fCostTime); 8 }
2.5 time函数
time是ANSI C标准库函数,其函数原型声明在time.h头文件中:
time_t time(time_t * timer); |
该函数返回当前的日历时间(以秒计)。若参数timer为非NULL指针,则时间值也通过该指针存储。若机器无法提供当前时间,或时间值过大而无法用time_t表示,则函数返回(time_t)-1。返回类型time_t通常定义为有符号长整型(long)。
测量
某程序执行时间时,可在待计时程序段起始和结束处分别调用time函数,用后一次的返回值减去前一次的返回值即可得到运行该程序所消耗的秒数。如:
1 #include <time.h> 2 void TimeTiming(void){ 3 time_t tBeginTime = time(NULL); 4 TimingFunc(); 5 time_t tEndTime = time(NULL); 6 double fCostTime = difftime(tEndTime, tBeginTime); 7 printf("[time]Cost Time = %fSec\n", fCostTime); 8 }
注意,时间类型time_t是个“可表示时间的算术类型(arithmetic type capable of representing times)”别名。但C标准并未规定time函数中该算术类型的时间编码方式。POSIX规定time函数必须返回一个时间整数,表示自Epoch(00:00 hours, Jan 1, 1970 UTC)以来的秒数;但库函数可能采用不同的时间表示方式。因此不应使用字面值常量,因其含义可能因编译器而异。
遵循POSIX规范的程序可直接对time_t对象进行算术运算;可移植程序则应调用相关标准库函数(如localtime、gmtime或difftime),将time_t对象转换为可移植类型。TimeTiming函数即使用difftime函数将先后调用time所获得的时间差值转换为秒。
Linux下time返回值为秒数,故difftime调用处等效于double fCostTime = (double)(tEndTime-tBeginTime)。注意,虽然difftime函数返回类型为double类型,但其值为以秒计的时间间隔,故只能精确到秒。
以下代码分别给出两种版本,以实现在至少dwWorkSec(秒)时间内多次执行TimingFunc:
1 #include <time.h> 2 int NoncompliantWork(int dwWorkSec){ 3 time_t tStart = time(NULL); 4 if(tStart == (time_t)(-1)) 5 return -1; 6 7 while(time(NULL) < tStart + dwWorkSec){ //时间编码方式未定义,故加法运算不能保证增加dwWorkSec秒 8 TimingFunc(); //Do some work 9 } 10 return 0; 11 } 12 int CompliantWork(int dwWorkSec){ 13 time_t tStart = time(NULL); 14 time_t tCurrent = tStart; 15 if(tStart == (time_t)(-1)) 16 return -1; 17 18 while(difftime(tCurrent, tStart) < dwWorkSec){ //因time_t表示范围所限,可能造成死循环(infinite loop) 19 TimingFunc(); //Do some work 20 tCurrent = time(NULL); 21 if(tCurrent == (time_t)(-1)) 22 return -1; 23 } 24 return 0; 25 }
2.6 clock_gettime函数
clock_gettime是POSIX1003.1实时函数,其函数原型声明在time.h头文件中:
int clock_gettime(clockid_t clk_id, struct timespec *tp); |
该函数获取tp关于指定时钟的当前timespec值,并将其存入指针tp所指结构体。其结构体定义为:
1 struct timespec{ 2 time_t tv_sec; //自1970年7月1日以来经过的秒数 3 long tv_nsec; //自上一秒开始经过的纳秒数(nanoseconds) 4 }
可见,该函数计时精度达到纳秒级。若函数执行成功,则返回0;否则返回一个错误码。
clockid_t值用于指定计时器的类型,POSIX.1b所支持的标准计时器如下:
- CLOCK_REALTIME:系统范围内的实时时钟,反映挂钟时间(wall clock time),即绝对时间。若系统时钟源被改变或系统时间被重置,该时钟会相应地调整。若指定该时钟类型,clock_gettime函数等效于gettimeofday函数,尽管精度有所不同。
- CLOCK_MONOTONIC:单调时间,不可设置。该时间通过jiffies值计算,其值为当前时间减去起始时间之差,即从系统启动至今所经过的时间。单调时间在运行期间会一直稳定增加,而不受系统时钟的影响。若指定该时钟类型,则tv_sec值与“cat /proc/uptime”第一个输出值(秒)相同。
- CLOCK_PROCESS_CPUTIME_ID:每个进程的CPU高精度硬件计时器。
- CLOCK_THREAD_CPUTIME_ID:每个线程的CPU高精度硬件计时器。
因为CLOCK_MONOTONIC
计时器更加稳定,故推荐以此获得系统的运行时间。结合/proc/uptime
文件,可通过以下几种方式获得
系统自举以来的秒数:
1 #include <fcntl.h> 2 #include <unistd.h> 3 //通过文件接口读取/proc/uptime中的值进行字符串的转换 4 int GetSysTime(int *pSec, int *pMsec){ 5 if(NULL == pSec && NULL == pMsec) 6 return -1; 7 8 int dwFd = open("/proc/uptime", O_RDONLY); 9 if(dwFd <= 0) 10 return -2; 11 12 char acReadBuf[128] = {0}; 13 if(read(dwFd, acReadBuf, sizeof(acReadBuf)) <= 0) 14 return -3; 15 16 int dwSecond = 0, dwMsecond = 0; 17 sscanf(acReadBuf, "%d.%d[^ ]", &dwSecond, &dwMsecond); 18 if(pSec != NULL) 19 *pSec = dwSecond; 20 if(pMsec != NULL) 21 *pMsec = dwMsecond; 22 23 close(dwFd); 24 return 0; 25 } 26 27 #include <sys/syscall.h> 28 //利用__NR_clock_gettime系统调用直接获取(编译链接时无需-lrt选项) 29 int GetSysTime2(int *pSec, int *pMsec){ 30 if(NULL == pSec && NULL == pMsec) 31 return -1; 32 33 struct timespec tSpec; 34 memset(&tSpec, 0, sizeof(tSpec)); 35 syscall(__NR_clock_gettime, CLOCK_MONOTONIC, &tSpec); 36 37 if(pSec != NULL) 38 *pSec = tSpec.tv_sec; 39 if(pMsec != NULL) 40 *pMsec = tSpec.tv_nsec/1000; 41 42 return 0; 43 } 44 45 int GetSysTime3(int *pSec, int *pMsec){ 46 if(NULL == pSec && NULL == pMsec) 47 return -1; 48 49 struct timespec tSpec; 50 memset(&tSpec, 0, sizeof(tSpec)); 51 clock_gettime(CLOCK_MONOTONIC, &tSpec); 52 53 if(pSec != NULL) 54 *pSec = tSpec.tv_sec; 55 if(pMsec != NULL) 56 *pMsec = tSpec.tv_nsec/1000; 57 58 return 0; 59 }
注意,/proc/uptime
文件
第二列输出为系统空闲的时间(以秒为单位),该时间计算时会计入SMP系统中所有逻辑CPU。
测量
某程序执行时间时,可在待计时程序段起始和结束处分别调用clock_gettime函数,用后一次获取的当前时间减去前一次获取的当前时间得到运行该程序所消耗的秒或微秒数。如:
1 #include <time.h> 2 void ClockGetTimeTiming(void){ 3 struct timespec tBeginTime, tEndTime; 4 clock_gettime(CLOCK_MONOTONIC, &tBeginTime); 5 TimingFunc(); 6 clock_gettime(CLOCK_MONOTONIC, &tEndTime); 7 double fCostTime = (tEndTime.tv_sec-tBeginTime.tv_sec) + 8 (double)(tEndTime.tv_nsec-tBeginTime.tv_nsec)/1000000000; 9 printf("[clock_gettime]Cost Time = %fSec\n", fCostTime); 10 }
注意,编译链接时需加上-lrt选项,因为clock_gettime函数在librt库中实现。
以下代码通过settimeofday函数将当前系统时间往回设置10秒,对比gettimeofday和clock_gettime所受的影响。注意,只有root权限才能调用settimeofday函数修改当前时间。
1 #include <time.h> 2 #include <unistd.h> 3 #include <sys/time.h> 4 5 void ChangeSysTime(void){ 6 struct timeval tv1, tv2; 7 struct timespec ts1, ts2; 8 9 gettimeofday(&tv1, NULL); 10 clock_gettime(CLOCK_MONOTONIC, &ts1); 11 12 struct timeval temp = tv1; 13 temp.tv_sec -= 10; 14 settimeofday(&temp, NULL); //将当前系统时间往回设置10秒 15 gettimeofday(&tv2, NULL); 16 clock_gettime(CLOCK_MONOTONIC, &ts2); 17 18 printf("gettimeofday: [%ld.%6ld ~ %ld.%6ld] => diff = %f\n", tv1.tv_sec, tv1.tv_usec, tv2.tv_sec, tv2.tv_usec, 19 ((tv2.tv_sec*1000000+tv2.tv_usec)-(tv1.tv_sec*1000000+tv1.tv_usec))/1000000.0); 20 printf("clock_gettime: [%ld.%9ld ~ %ld.%9ld] => diff = %f\n", ts1.tv_sec, ts1.tv_nsec, ts2.tv_sec, ts2.tv_nsec, 21 ((ts2.tv_sec*1000000000+ts2.tv_nsec)-(ts1.tv_sec*1000000000+ts1.tv_nsec))/1000000000.0); 22 23 tv2.tv_sec += 10; 24 settimeofday(&tv2, NULL); //恢复系统时间 25 gettimeofday(&tv2, NULL); 26 printf("gettimeofday2: [%ld.%6ld]\n", tv2.tv_sec, tv2.tv_usec); 27 }
执行结果输出如下:
可见,当系统时间被人为改动时,gettimeofday函数计算的时间差存在偏差;clock_getime函数计时则不受影响,仅与实际所经历的时间相关。
2.7 getrusage函数
getrusage函数来自BSD系统,其函数原型声明在sys/resource.h头文件中:
int getrusage(int who, struct rusage *usage); |
该函数获取当前进程或其所有已终止的子进程的资源使用信息,并将其存入指针usage所指结构体。该结构体定义为:
1 struct rusage{ 2 struct timeval ru_utime; //time spent executing in user mode 3 struct timeval ru_stime; //time spent in the system executing on behalf of the process 4 long ru_maxrss; //maximum resident set size utilized(in kilobytes) 5 long ru_ixrss; //integral value indicating the amount of memory used by the text segment shared among other processes, expressed in units of kilobytes * ticks-of-execution. Ticks refer to a statistics clock that has a frequency of sysconf(_SC_CLOCK_TCK) ticks per second. 6 long ru_idrss; //integral value of the amount of unshared memory residing in the data segment of a process(expressed in units of kilobytes * ticks-of-execution) 7 long ru_isrss; //integral value of the amount of unshared memory residing in the stack segment of a process(expressed in units of kilobytes * ticks-of-execution) 8 long ru_minflt; //number of page faults serviced without any I/O activity; here I/O activity is avoided by ''reclaiming'' a page frame from the list of pages awaiting reallocation 9 long ru_majflt; //number of page faults serviced that required I/O activity 10 long ru_nswap; //number of times a process was ''swapped'' out of main memory 11 long ru_inblock; //number of times the file system had to perform input(account only for real I/O) 12 long ru_oublock; //number of times the file system had to perform output(account only for real I/O) 13 long ru_msgsnd; //number of IPC messages sent 14 long ru_msgrcv; //number of IPC messages received 15 long ru_nsignals; //number of signals delivered 16 long ru_nvcsw; //voluntary context switches 17 long ru_nivcsw; // involuntary context switches 18 };
在rusage结构体中,Linux仅维护ru_utime/ru_stime/ru_minflt/ru_majflt/ru_nswap等字段。其中,用户时间(ru_utime)和系统时间(ru_stime)与times函数tms结构体内容相似,但由结构体timeval来保存(而不是含义模糊的clock_t)。在Linux中,getrusage使用的时钟频率由正在运行的内核决定。clock_t时间间隔可能是10ms,而getrusage获得的tick时间间隔可能是1ms(Linux 2.6内核tick频率为1000Hz,而用户频率却为100Hz)。因此,getrusage函数的计时精度将比times函数更高。
参数who的取值可为RUSAGE_SELF(获取当前进程的资源使用信息)或RUSAGE_CHILDREN(获取子进程的资源使用信息),根据该值将当前进程或其子进程的信息填入rusage结构。
若函数执行成功,则返回0;否则返回-1,并设置全局变量errno以指示相关错误。
测量某程序执行时间时,可在待计时程序段起始和结束处分别调用getrusage函数,用后一次获取的当前时间减去前一次获取的当前时间得到运行该程序所消耗的秒或微秒数。如:
1 #include <sys/resource.h> 2 void GetRusageTiming(void){ 3 struct rusage tBeginResource, tEndResource; 4 getrusage(RUSAGE_SELF, &tBeginResource); 5 TimingFunc(); 6 getrusage(RUSAGE_SELF, &tEndResource); 7 unsigned dwCostSec = (tEndResource.ru_utime.tv_sec-tBeginResource.ru_utime.tv_sec) + 8 (tEndResource.ru_stime.tv_sec-tBeginResource.ru_stime.tv_sec); 9 unsigned dwCostUsec = (tEndResource.ru_utime.tv_usec-tBeginResource.ru_utime.tv_usec) + 10 (tEndResource.ru_stime.tv_usec-tBeginResource.ru_stime.tv_usec); 11 printf("[getrusage]Cost Time = %dSec, %dUsec\n", dwCostSec, dwCostUsec); 12 }
当应用程序创建进程或使用线程时,计量出的时间会随着应用程序和计时函数的变化而不同。尤其是当应用程序创建一个子进程,而该子进程随后通过wait系统调用被收养时,父进程的运行时间数据将包含其子进程的运行时间。若进程忽略回收子进程,time将无法反映该子进程的运行时间。此时,可通过函数getrusage的参数who来控制想得到的数据。当选用RUSAGE_CHILDREN标志时,回馈的时间只包括收养后的子进程的运行时间。直到父进程调用wait为止,返回的时间将是0。然而,这对进程中的线程不成立。因为线程不是子进程,故线程消耗的时间也认为是进程所耗时间。即使未进行其他系统调用,由getrusage测量出的时间也会因为线程的运行而增大。
2.8 函数批量计时
此处简要描述如何使用C语言方便地测量一批函数的运行时间。
为方便起见,假定待测函数均不带参数且返回类型相同(其他情况稍加封装即可)。为消除计时和输出代码的冗余,使用循环和函数指针依次实现调用同类型的待测函数,代码示例如下:
1 int CalcMul(void) {int a=9999, b=135; return a*b;} 2 int CalcDiv(void) {int a=9999, b=135; return a/b;} 3 int CalcMod(void) {int a=9999, b=135; return a%b;} 4 typedef int (*FTiming)(void); 5 typedef struct{ 6 FTiming fnTimingFunc; 7 char* pszFuncName; 8 }T_FUNC_MAP; 9 #define FUNC_ENTRY(funcName) {funcName, #funcName} 10 T_FUNC_MAP TimingFuncMap[] = { 11 FUNC_ENTRY(CalcMul), 12 FUNC_ENTRY(CalcDiv), 13 FUNC_ENTRY(CalcMod) 14 }; 15 const unsigned FUNC_MAP_NUM = (unsigned)(sizeof(TimingFuncMap)/sizeof(T_FUNC_MAP)); 16 17 void BatchTiming(void){ 18 struct timeval tBeginTime, tEndTime; 19 unsigned iFuncIdx = 0; 20 for(iFuncIdx = 0; iFuncIdx < FUNC_MAP_NUM; iFuncIdx++){ 21 gettimeofday(&tBeginTime, NULL); 22 TimingFuncMap[iFuncIdx].fnTimingFunc(); 23 gettimeofday(&tEndTime, NULL); 24 float fCostTime = 1000000*(tEndTime.tv_sec-tBeginTime.tv_sec) + 25 (tEndTime.tv_usec-tBeginTime.tv_usec); 26 printf("[%s]Cost: %fSec\n", TimingFuncMap[iFuncIdx].pszFuncName, fCostTime/1000000); 27 } 28 }
示例中TimingFuncMap初始化列表仅注册三个函数(CalcMul等)。当待测函数多达数百以上时,可借助工具提取源文件中所有函数名。
批量计时应注意以下几点:
1) 多次运行待测函数取均值可减小统计误差,得出较为精确的运行时间。但要注意待测函数耗时应远大于循环指令执行时间,且需考虑清空高速缓存。
2) 批量计时过程中,若系统时钟被改变,则gettimeofday函数将依据新的时间来计时,导致计时偏差。此时可选用不受系统时间影响的函数(如clock、times等)。
三 总结
对比本文所述的各种计时方式,如下表所示:
计时方式 | 通用性 | 精度 | 计时范围 |
time命令 | Linux | 10毫秒(ms) | / |
clock函数 | ANSI C | 10毫秒(ms) | (231-1)/1000000/60≈35分 |
times函数 | ANSI C | 10毫秒(ms) | (231-1)/100/3600/24≈243天 |
rdtsc指令 | I386 | 1纳秒(ns) | 取决于CPU主频(主频为1GHz时约583年) |
time函数 | ANSI C | 1秒(s) | (231-1)/3600/24/365≈68年 |
gettimeofday函数 | ANSI C | 1微秒(μs) | ((231-1)+(231-1)/1000000)/3600/24/365≈68年 |
clock_gettime函数 | POSIX | 1纳秒(ns) | 约同gettimeofday函数 |
getrusage函数 | POSIX | 1微秒(μs) | 同gettimeofday函数 |
1秒(second) = 1,000毫秒(millisecond) = 1,000,000微秒(microsecond) = 1,000,000,000纳秒(nanosecond) |