我們寫程序的時候經常會使用計時函數,比如RPC中計算超時時間,日志中打印當前時間,性能profile中計算某個函數的執行時間等。在使用時間函數的時候,我們一般默認認為時間函數本身的性能是很高的,對主邏輯的影響可以忽略不計。雖然絕大部分情況下這個假設都沒問題,但是了解更多細節能夠增加我們對問題的把控力,利於系統的設計和問題的調查。
首先來比較gettimeofday/clock_gettime的性能。
程序代碼見后
Glibc版本:
$rpm -qa|grep glibc-comm
glibc-common-2.5-81
內核版本:
$uname -a
2.6.32-220.23.2
$./a.out -help
[gettimeofday/clock_gettime] thread_number loop_count
$./a.out gettimeofday 1 100000000
gettimeofday(50035480681901) , times : 100000000
thread 1105828160 consume 4000225 us
單線程gettimeofday大概每次40ns
圖1看出,gettimeofday走的是vsyscall[1](虛擬系統粗糙的描述就是不經過內核進程的切換就可以調用一段預定好的內核代碼),沒有線程切換的開銷。
圖1 gettimeofday 走vsyscall
圖2 gettimeofday能將usr態cpu消耗壓到100%
因為走vsyscall,沒有線程切換,所以多線程運行性能跟單線程是類似的。
$./a.out gettimeofday 12 100000000
gettimeofday(51127820371298) , times : 100000000
thread 1201568064 consume 4111854 us
$./a.out clock_gettime 1 100000000
clock_gettime(50265567600696623) , times : 100000000
thread 1107867968 consume 10242448 us
單線程clock_gettime大概每次100ns
圖3 clock_gettime 走真正的系統調用
圖4 clock_gettime 70%的cpu花在sys態,確實進入了系統調用流程
因為開銷集中在系統調用本身,而不是花在進程切換上,所以多線程結果跟單線程類似。
$./a.out clock_gettime 12 100000000
clock_gettime(50369061997211567) , times : 100000000
thread 1122031936 consume 10226828 us
這里說“開銷集中在系統調用本身”意思是說clock_gettime本身的執行就非常耗費時間,其大概的調用路徑是
clock_gettime -> sys_call -> sys_clock_gettime -> getnstimeofday -> read_tsc -> native_read_tsc
void getnstimeofday(struct timespec *ts) { unsigned long seq; s64 nsecs; WARN_ON(timekeeping_suspended); do { // 下面代碼執行過程中xtime可能會被更改,這里通過持有一個序號來避免顯示加鎖,如果該代碼執行完畢之后,seq並未改變,說明xtime未被更改,此次執行成功,否則重試;無論這里重試與否,CPU都會一直干活; seq = read_seqbegin(&xtime_lock); *ts = xtime; nsecs = timekeeping_get_ns(); //從當前時鍾源取更細致的時間精度 /* If arch requires, add in gettimeoffset() */ nsecs += arch_gettimeoffset(); // 中斷從發出到執行有時間消耗,可能需要做補償 } while (read_seqretry(&xtime_lock, seq)); timespec_add_ns(ts, nsecs); //時間轉換 } /* Timekeeper helper functions. */ static inline s64 timekeeping_get_ns(void) { cycle_t cycle_now, cycle_delta; struct clocksource *clock; /* read clocksource: */ clock = timekeeper.clock; // 使用系統注冊的時鍾源[2]來讀取,當前情況下,一般tsc是默認時鍾源 // /sys/devices/system/clocksource/clocksource0/current_clocksource // 下面的調用最終執行native_read_tsc, 里面就是一條匯編指令rdtsc cycle_now = clock->read(clock); /* calculate the delta since the last update_wall_time: */ cycle_delta = (cycle_now - clock->cycle_last) & clock->mask; /* return delta convert to nanoseconds using ntp adjusted mult. */ return clocksource_cyc2ns(cycle_delta, timekeeper.mult, timekeeper.shift); }
上面說到時鍾源是可以替換的,
$cat /sys/devices/system/clocksource/clocksource0/available_clocksource
tsc hpet acpi_pm
這是所有可用的時鍾源。硬件出問題或者內核bug有時候會使得tsc不可用,於是時鍾源默認會切換到hpet,使用hpet后gettimeofday和clock_gettime性能如何?測試一下。
$sudo bash -c "echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource"
$cat /sys/devices/system/clocksource/clocksource0/current_clocksource
hpet
$./a.out gettimeofday 1 100000000
gettimeofday(50067118117357) , times : 100000000
thread 1091926336 consume 71748597 us
延時是原來的17倍,大概700ns一次;clock_gettime 與此類似,因為此時瓶頸已經不是系統調用,而是hpet_read很慢。
此時大概的調用路徑是
clock_gettime -> sys_call -> sys_clock_gettime -> getnstimeofday -> read_hpet -> hpet_readl –> readl
實現非常直白,但是readl是讀時鍾設備的內存映射,慢是肯定的了。
總結來說,上文制定的內核和glibc版本下,tsc時鍾源,gettimeofday 比 clock_gettime快1倍多,適合做計時用(clock_gettime使用CLOCK_REALTIME_COARSE也是很快的);如果因為tsc不穩定(硬件或者內核bug都可能導致,碰到過),hpet一般不會同時出問題,這時hpet成為了新的時鍾源,整體性能下降數十倍,兩者沒啥區別了。
[1]. On vsyscalls and the vDSO : http://lwn.net/Articles/446528/
[2]. Linux內核的時鍾中斷機制 : http://wenku.baidu.com/view/4a9f37f24693daef5ef73d32.html
#include <sys/time.h> #include <iostream> #include <time.h> using namespace std; uint64_t now() { struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec * 1000000 + tv.tv_usec; } void* func_gettimeofday(void* p) { int32_t c = *(int32_t*)p; uint64_t start = now(); uint64_t us = 0; int i = 0; while (i++ < c) { struct timeval tv; gettimeofday(&tv, NULL); us += tv.tv_usec; // avoid optimize } cout << "gettimeofday(" << us << ") , times : " << c << endl; cout << "thread " << pthread_self() << " consume " << now() - start << " us" << endl; return 0; } void* func_clockgettime(void* p) { int32_t c = *(int32_t*)p; uint64_t start = now(); uint64_t us = 0; int i = 0; while (i++ < c) { struct timespec tp; clock_gettime(CLOCK_REALTIME, &tp); us += tp.tv_nsec; } cout << "clock_gettime(" << us << ") , times : " << c << endl; cout << "thread " << pthread_self() << " consume " << now() - start << " us" << endl; return 0; } int main(int argc, char** argv) { if (argc != 4) { cout << " [gettimeofday/clock_gettime] thread_number loop_count" << endl; exit(-1); } string mode = string(argv[1]); int n = atoi(argv[2]); int loop = atoi(argv[3]); pthread_t* ts = new pthread_t[n]; for (int i = 0; i < n; i++) { if (mode == "gettimeofday") { pthread_create(ts+i, NULL, func_gettimeofday, &loop); } else { pthread_create(ts+i, NULL, func_clockgettime, &loop); } } for (int i = 0; i < n; i++) { pthread_join(ts[i], NULL); } delete [] ts; return 0; }