轉自: http://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...>] 或 ime [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 ime --output=hello.txt find . -name "hello.c"
4 ime find . -name "hello.c" 2> hello.txt
若還要包括find指令執行的結果,則可用:
ime --output=hello.txt --append find . -name "hello.c" > hello.txt
若要控制輸出時間的格式,可使用-f選項進行格式化(格式控制符用法見相關手冊):
ime -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
", 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
", (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
", 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
", 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!
", __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
", 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
", (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
", 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
", 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
", 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
", 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
", 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]
", 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
", 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
", 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) |