Linux下clock計時函數學習

平時在Linux和Winows下都有編碼的時候,移植代碼的時候免不了發現一些問題。
1. 你到底準不準?關於clock()計時函數
首先是一段簡單的測試代碼,功能爲測試從文本文件讀取數據並賦值給向量最後打印輸出的運行時間。
 

int main(int argc, char **argv)
{    
clock_t t1=clock();
ifstream in("data.txt");
vector<int> v;
for(int a;in>>a;v.push_back(a));
cout<<v.size()<<endl;
for(int i=0;i<v.size();i++)
cout<<v[i]<<" ";
cout<<endl;
clock_t t2=clock();
cout<<"TotalTime:"<<t2-t1<<"ms"<<endl;
}


這段代碼中用了兩個clock_t類型的clock()函數來計算程序運行時間,

在windows下編譯運行,
TotalTime:465ms

在Linux下編譯運行,
TotalTime:420000ms

ok,問題來了,除了正常碼農都能理解的系統誤差原因,明顯是由於clock()函數在不同平臺下的返回值不同原因造成的,查clock函數定義有下面的描述:

clock returns the processor time used by program since the beginning of the execution, or -1 if unavailable.

這裏提到clock()函數返回的是程序運行過程中耗掉得process time,也就是CPU time。
你以爲它返回的是一個標準時間單位,你錯了,因爲還有下一句描述:
clock() / CLOCKS_PER_SEC is a time in seconds.
CLOCKS_PER_SEC,它用來表示一秒鐘會有多少個時鐘計時單元,也就是硬件滴答數。
先不管什麼叫硬件滴答數,你需要知道clock()是基於時鐘計時單元(clock tick)的這個東西來計數的。一個clock tick不是CPU的一個時鐘週期,而是C/C++的一個基本計時單位,因此只與編譯器有關。在TC2.0中硬件每18.2個滴答是一秒,在VC中硬件每1000個滴答是一秒,在標準POSIX中定義爲1000000個滴答爲一秒。

ok,知道了這麼多,要想讓程序正確運行輸出,並保證多平臺的兼容性,代碼作如下處理:
cout<<"TotalTime:"<<(double)( t2-t1)/CLOCKS_PER_SEC<<"s"<<endl;

很簡單,程序正確輸出之後,你迫不及待的多次運行測試,發現無論如何你的程序都是一個精確到10毫秒的整數,真是因爲每次都那麼巧麼?經搜索發現,標準POSIX平臺的clock()只能精確到10ms,至於爲什麼我目前還不得而知。

2. 做個理性的偏執狂,關於系統計時誤差
    如果你是一個偏執狂,當你正爲使用clock()函數丟失了幾毫秒而懊惱不已時,我卻你還是回家陪你老婆逛逛街吧。因爲精確測量某一個程序運行的確切時間是根本不可能的,至少現在是這樣。我不厭其煩的從其他文章中找到這樣的一些話,希望對你理解爲何有誤差有所幫助。
    文章中提到“我們平時常用的測量運行時間的方法並不是那麼精確的,換句話說,想精確獲取程序運行時間並不是那麼容易的。也許你會想,程序不就是一條條指令麼,每一條指令序列都有固定執行時間,爲什麼不好算?真實情況下,我們的計算機並不是只運行一個程序的,進程的切換,各種中斷,共享的多用戶,網絡流量,高速緩存的訪問,轉移預測等,都會對計時產生影響。
    對於進程調度來講,花費的時間分爲兩部分,第一是計時器中斷處理的時間,也就是當且僅當這個時間間隔的時候,操作系統會選擇,是繼續當前進程的執行還是切換到另外一個進程中去。第二是進程切換時間,當系統要從進程A切換到進程B時,它必須先進入內核模式將進程A的狀態保存,然後恢復進程B的狀態。因此,這個切換過程是有內核活動來消耗時間的。具體到進程的執行時間,這個時間也包括內核模式和用戶模式兩部分,模式之間的切換也是需要消耗時間,不過都算在進程執行時間中了”。

        ok,讀完上面這段話,你應該很清楚的知道一個程序即使每次執行相同的命令,所花費的時間也不一定相同,因爲其花費的時間與系統運行相關。原因就在於無論是windows還是linux,都是多任務操作系統。爲了讓你更明白,現在從另一個角度對執行一個程序所消耗的時間進行分類,如下:

(1) 實際運行時間(real time):從命令行執行到運行終止的消逝時間;

(2) 用戶CPU時間(user CPU time):命令在用戶態中執行時間的總和;

(3) 系統CPU時間(system CPU time):命令在系統核心態中執行時間的總和。

       現在有沒有這樣的疑問,實際運行時間(1)是不是等於(2)+(3)?對於一個進程而言,除了用戶和系統之外難道還有別人麼?

答案是肯定的。擡頭看看上面提到的進程調度,回頭想想你大二學的操作系統課程裏的時間片輪轉,進程五狀態,明白了吧,此時的程序表面在給你執行代碼,CPU早跑別人家去了。

                 ok,現在回頭看看開頭那段代碼的運行結果,clock()算出的時間究竟屬於上面的哪一個呢?爲此我們設計如下一個實驗:
該段代碼爲測試執行一億次空循環所消耗的時間
 

int main( void )
{
clock_t start, finish;
double duration;
long i,j;
start = clock();
for( i=0;i<100;i++){
for( j=0;j<1000000;j++);
}
finish = clock();
duration = (double)(finish- start) / CLOCKS_PER_SEC;
printf( "Time to do %ld empty loops is ",i*j);
printf( "%f seconds\n",duration );
return 0;
}



編譯連接
g++ test.cpp -o test
運行程序
./test
顯示結果
Time to do 100000000 empty loops is 0.290000 seconds
僅這條結果當然證明不了什麼,好吧,我們在其中加一句

sleep(5); // 需要包含頭文件<unistd.h>

sleep函數會使程序暫時掛起,也就是阻塞狀態,在這5s內,CPU並不在該進程上花費時間。如果最後顯示的時間大於5s的話,可以證明clock函數計算的是程序實際運行時間,如果依然接近290ms的話,則是CPU運行時間。

運行程序 ./test

顯示結果
Time to do 100000000 empty loops is 0.290000 seconds

一樣!現在證明了clock函數是計算的CPU時間,聰明且記憶力好的你此時會發現我有欺騙大衆智商的嫌疑,clock的定義裏

明確之處計算的是processor time,不就是CPU時間啊,好吧,我就算設計一個實驗證實了這個說法沒錯,但究竟clock函數計算的是哪個CPU時間,暫時是無能爲力證明了。

偏執狂你的開始不滿意了,究竟有沒有辦法分別統計一個程序的這三樣時間呢?答案是有的。

Linux下有一個很簡單很好用的命令就可以來統計程序運行的這三種時間——time命令。此命令的用途在於測量特定指令執行

時所需消耗的時間及系統資源等資訊。如果你只是在優化一小段精簡的代碼,這對你來說是個好消息。

下面用time命令運行上面的那段測試代碼:

運行shell命令

time ./test

顯示結果

Time to do 100000000 empty loops is 0.290000 seconds

real 0m0.915s
user 0m0.031s
sys 0m0.266s

    現在簡單分析結果,終於真相大白了。上面程序中使用clock()函數計算出來的時間就爲總的CPU時間。也就是說,clock函數不能區分用戶空間和內核空間。
    上面介紹的time命令能測量特定進程執行時所消耗的時間,它是怎麼做到的呢?方法叫做間隔計數。如果你對他感興趣的話,

可以繼續讀讀下面的介紹,你會發現原理其實很簡單。

操作系統用計時器來記錄每個進程使用的累計時間,計時器中斷髮生時,操作系統會在當前進程列表中尋找哪個進程是活動的,一

旦發現進程A正在運行立馬就給進程A的計數值增加計時器的時間間隔(這也是引起較大誤差的原因)。當然不是統一增加的,還要確定這個進程是在用戶空間活動還是在內核空間活動,如果是用戶模式,就增加用戶時間,如果是內核模式,就增加系統時間。這種方法的原理雖然簡單但不精確。如果一個進程的運行時間很短,短到和系統的計時器間隔一個數量級,用這種方法測出來的結果必然是不夠精確的,頭尾都有誤差。不過,如果程序的時間足夠長,這種誤差有時能夠相互彌補,一些被高估一些被低估,平均下來剛好。從理論上很難分析這個誤差的值,所以一般只有程序達到秒的數量級時用這種方法測試程序時間纔有意義。這種方法最大的優點是它的準確性不是非常依賴於系統負載。

    3. 事實果真如此麼?關於”三多“話題

多核多進程多線程技術的發展使得運算效率不斷提高的同時,也給碼農們帶來新的煩惱。

3.1 多核計算
上面已經知道了clock函數的實現是基於時鐘計時單元的。問題就出在了cpu的時鐘計時單元上。當採用多核cpu時,進程或線程調

用clock,記錄了當前核時鐘。但在下次調用clock之前很可能發生cpu調度,進程或線程被調度到其他cpu上運行。這導致兩次取得

計時單元並不是同一個cpu的,產生計時錯誤。但究竟這個誤差有多大,有待實驗論證。

3.2 多進程計算

上面通過time函數進行了驗證,clock函數計算的時間貌似是等於用戶CPU時間+系統CPU時間。果真如此麼?我們再次對測試代碼

進行修改,將循環次數減少至一千次,並在空循環里加一句

system("cd");

這一句用於模擬子進程的運行,這裏圖方便選擇系統進程作爲子進程。

運行shell命令

time ./test

顯示結果

Time to do 1000 empty loops is 0.010000 seconds

real 0m3.492s
user 0m0.512s
sys 0m2.972s

這個實驗說明了,clock函數並沒有考慮CPU被子進程消耗的時間。

3.3 多線程計算

上面提到了三個時間Real time, User time和Sys time。real time > user time + sys time 這種關係始終成立麼?

答案是否定的。原因就在於並行計算。現在再次回憶一下這三種時間的概念:

Real指的是實際經過的時間,User和Sys指的是該進程使用的CPU時間。
1. Real是牆上時間(wall clock time),也就是進程從開始到結束所用的實際時間。這個時間包括其他進程使用的時間片和進程阻塞的時間(比如等待I/O完成)。
2. User指進程執行用戶態代碼(核心之外)所使用的時間。這是執行此進程所消耗的實際CPU時間,其他進程和此進程阻塞的時間並不包括在內。
3. Sys指進程在內核態消耗的CPU時間,即在內核執行系統調用所使用的CPU時間。
那麼,什麼情況下進程開始到結束所經過的時間會比進程所消耗的用戶時間和系統時間(user time + sys time)小呢?
User+Sys爲進程所使用的實際CPU時間。在多處理器的系統上,一個進程如果有多個線程或者有多個子進程並行執行,就可能導致Real time比CPU time(User + Sys time)要小,這是很容易理解的。

 

注:轉載僅作爲筆記使用,如有侵權,請聯繫!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章