Linux用户态程序计时方式详解(1)(2)
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章中,深入讨论了这些因素对计时的影响以及尽可能获取精确计时的方法。
评论暂时关闭