原文地址:https://www.cnblogs.com/clover-toeic/p/3845210.html
前言
良好的計時器可幫助程序開發人員確定程序的性能瓶頸,或對不同算法進行性能比較。但要精確測量程序的運行時間並不容易,因為進程切換、中斷、共享的多用戶、網絡流量、高速緩存訪問及轉移預測等因素都會對程序計時產生影響。
本文將不考慮這些影響因素(相關資料可參考《深入理解計算機系統》一書),而僅僅關注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) |