歡迎光臨本專欄,這個新的 Linux 專欄主要示範和比較了 Linux 和 Windows 2000 作業系統的效能。專欄作家 Ed Bradford 比較了作業系統級的特性,而不是應用程式,以便讓人們瞭解每個作業系統的最佳效能特性。本文包含了原始碼,在儘可能公平的環境中,它們表示每個平台的“最佳編程執行個體”。
在這個新的文章系列中,將主要討論用於 Linux 和 Windows 2000 作業系統的高效能編程技術。我將示範實用且有效編程執行個體,它們能夠解決 Linux 和 Windows 2000 上出現的同一問題。問題解決之後,就至少可以對每個平台進行某一方面的效能測量。各個效能測試指令碼和程式將會顯示作業系統特性的速度。我的目標是示範如何得到每個作業系統可能的最佳效能,順便比較一下這兩個操作平台的效能。
效能測試概述
這些測試將檢測記憶體速度、系統調用速度、輸入輸出總和、環境切換速度和許多其它在這兩種操作平台上通用的編程工具。但是,不會測量對 Windows 註冊表的訪問。本文中發表了原始碼,也可以免費下載這些原始碼。在這裡追求的是富有建設性的評價。我的目的是首先展示最好的編程執行個體 -- 然後比較效能。十分歡迎讀者在 討論論壇上針對本文發表您的看法。
我們將 Linux 看作是兩個作業系統:Linux 2.2.16 核心和 2.4.2 核心。Windows 2000 是指版本 Windows 2000 2195 系統,發行時,稱作 Windows XP 核心。所有測試都基於完全相同的硬體。首選硬體是雙引導 IBM ThinkPad 600X(帶 576 MB 記憶體和兩個 12-GB 磁碟)。
雖然很少用到物件導向的代碼,但還是用 C++ 編寫了程式。使用 C++ 的原因只是 C 具有很強的類型檢查特點。在 Linux 上,使用 Red Hat 7.0 分發版所帶的 gcc。在 Windows 2000 上,使用 Visual Studio 6.0 下的 Microsoft C++ 版本 12.00.8168。
測量公用程式
首篇文章定義了對 Windows 2000 和 Linux 進行測量和報告測量結果所需的實用例行程式。所列出的工具很少:用於測量時間的介面、返回描述作業系統的字串的常式、簡單 malloc() 記憶體配置常式的簡單且有效介面,以及處理大數的輸入常式。(以下將詳細討論定時常式)。
這裡的記憶體 Clerk稱為 Malloc(int)。它所做的就是調用 malloc(int) 常式,如果 malloc(int) 失敗,Malloc() 列印一條錯誤訊息並退出。在測試記憶體配置效能時,沒有用這個常式,但同時,它是流線形編碼。這裡使用 malloc() 是由於在 Windows 2000 和 Linux 中都有這個函數。Malloc() 在這兩個系統中是公平而等同的。
接下來的一個常式是 atoik(char *)。這個常式與 atoi() 常式是相同的,但它帶一個尾碼 "k" 或 "m"。尾碼 "k" 或 "m" 表示:對於 "k",將已解析的數字乘以 1024,對於 "m",將已解析的數字乘以 1024*1024。"k" 和 "m" 可以是大小寫,並且可以給它們附加任何數字。Atoik() 只返回 32 位,所以當出現任何大於或等於 2 GB 的數字時將不能繼續運行。當出現這個問題時,使用我們編寫的 atoik64() 函數。這個常式在兩個作業系統中是相同的。
測量時間
在這兩個作業系統上如何測量時間?讓我們看一下我們有幾種選擇。在 Windows 2000 中有兩個 API 可以測量時間間隔。第一個是 GetTickCount()。這個函數報告自從系統啟動後經過的毫秒數。GetTickCount() 是以“時鐘報時訊號”為顆粒度。這意味著只有當系統發出時鐘報時訊號時這個函數才會更新這個值。在 Windows 中,這個更新間隔為 10 毫秒。所以它的顆粒度不超過 10 毫秒或 10000 微妙。
Windows 2000 還有一個 QueryPerformanceCounter() API,它用來重新修正 64 位元高解析度效能計數器的當前值。調用 QueryPerformanceCounter() 的結果中的每次“報時”取決於 QueryPerformanceFrequency() 返回的值。頻率是每秒計數器的增加量,所以秒可以表示成:
用 QueryPerformanceCounter() 計算秒
LARGE_INTEGER tim, freq; double seconds; QueryPerformanceCounter(&tim); QeryPerformanceFrequency(&freq); seconds = (double)tim / (double) freq; |
由於 GetTickCount() 的解析度太低,我們將只使用 QueryPerformanceCounter。我們必須要注意,如果計時短到與 QueryPerformanceCounter() API 的開銷一樣時,那麼我們的結果可能時不可靠的。下面我們將測量計時常式的開銷。
在 Linux 上,使用 gettimeofday() API。只有這個 API 可以滿足我們在次毫秒級時間的需求。
選擇完要使用的 API 後,需要定義自己的 API,這樣可以使程式在不知道主機作業系統的情況下也可以使用這些 API。我們選用下面的介面來實現這些功能:
計時常式介面
void tstart(); void tend(); double tval(); |
當調用 Tstart() 時,它記錄靜態記憶體中的時間值。當調用 Tend() 時,它記錄靜態記憶體中的時間值。Tval() 採用 tstart 和 tend 時間值,將它們轉換為雙精確度數,然後減去它們,以返回雙精確度形式的結果。這個介面在 Linux 和 Windows 上很容易實現,它執行了所需要的計時功能。
Linux 和 Windows 2000 下計時常式的實現如下。由於不能避免對系統的依賴性,所以我們的目標是在儘可能地減小條件定義的情況下,編寫最佳的代碼。以下是計時常式的清單。
計時常式
#ifdef _WIN32 static LARGE_INTEGER _tstart, _tend; static LARGE_INTEGER freq; void tstart(void) { static int first = 1; if(first) { QueryPerformanceFrequency(&freq); first = 0; } QueryPerformanceCounter(&_tstart); } void tend(void) { QueryPerformanceCounter(&_tend); } double tval() { return ((double)_tend.QuadPart - (double)_tstart.QuadPart)/((double)freq.QuadPart); } #else static struct timeval _tstart, _tend; static struct timezone tz; void tstart(void) { gettimeofday(&_tstart, &tz); } void tend(void) { gettimeofday(&_tend,&tz); } double tval() { double t1, t2; t1 = (double)_tstart.tv_sec + (double)_tstart.tv_usec/(1000*1000); t2 = (double)_tend.tv_sec + (double)_tend.tv_usec/(1000*1000); return t2-t1; } #endif |
最後一個的常式是 "char *ver()"。這個簡單的函數返回一個描述當前作業系統環境的字串。正如在原始碼中所見,它在每個操作平台上都完全不同。在該常式結尾處是用於測試的條件性定義的 main() 常式。編譯過程如下:
將 ver.cpp 編譯成為程式
gcc -DMAIN -O2 ver.cpp -o ver 或 cl -DMAIN -O2 ver.cpp -o ver.exe |
ver.exe 程式用於將作業系統版本資訊記錄到輸出檔案。
Ver.cpp - 列印作業系統版本
#ifdef _WIN32 #include <windows.h> #else #include <sys/utsname.h> #endif #include <stdio.h> int ver_underbars = 0; char *ver() { char *q; #ifdef _WIN32 static char verbuf[256]; #else static char verbuf[4*SYS_NMLN + 4]; #endif #ifdef _WIN32 OSVERSIONINFO VersionInfo; VersionInfo.dwOSVersionInfoSize = sizeof(VersionInfo); if(GetVersionEx(&VersionInfo)) { if(strlen(VersionInfo.szCSDVersion) > 200) VersionInfo.szCSDVersion[100] = 0; sprintf(verbuf, "Windows %d.%d build%d PlatformId %d SP=/"%s/"", VersionInfo.dwMajorVersion, VersionInfo.dwMinorVersion, VersionInfo.dwBuildNumber, VersionInfo.dwPlatformId, VersionInfo.szCSDVersion); } else { strcpy(verbuf, "WINDOWS UNKNOWN"); } #else struct utsname ubuf; if(uname(&ubuf)) { strcpy(verbuf, "LINUX UNKNOWN"); } else { sprintf(verbuf,"%s %s %s %s", ubuf.sysname, ubuf.release, ubuf.version, ubuf.machine); } #endif // Substitute an underbar for white space. Makes output // easier to parse. if(ver_underbars) { for(q = verbuf; *q; q++) if(*q == ' ' || *q == '/t' || *q == '/n' || *q == '/r' || *q == '/b' || *q == '/f') *q = '_'; } return verbuf; } // gcc -DMAIN ver.cpp -o ver -- produces a simple test program. #ifdef MAIN int main(int ac, char *av) { if(ac > 1) ver_underbars = 1; printf("%s/n", ver()); return 0; } #endif |
上面定義的計時函數可以滿足我們的需要。開始使用它們之前,應該知道它們要執行多久。實際上,我們只需要知道 tstart() 和 tend() 要執行多長時間。由於這兩個函數在形式上是一樣的,因此只需要計算其中一個的執行時間。在 Windows 2000 和 Linux 中,使用 time-timers.cpp 程式對計時函數進行計時分析。請注意,這裡只列出了 main() 常式。實際程式包括所有計時器函數和前面清單中的 atoik() 原始碼。
time-timers.cpp - 計時定時器的程式
char *applname; int main(int ac, char *av[]) { long count = 100000; long i; double t; char *v = ver(); char *q; applname = av[0]; if(strrchr(applname,SLASHC)) applname = strrchr(applname,SLASHC) + 1; if(ac > 1) { count = atoik(av[1]); ac--; av++; if(count < 0) count = 100000; } tstart(); for(i = 0; i < count; i++) tend(); tend(); t = tval(); printf("%s: ",applname); printf("%d calls to tend() = %8.3f seconds %8.3f usec/call/n", count, t, (t/( (double) count ))*1E6); return 0; } |
一切就緒。現在編譯 time-timers.cpp 程式,方式如下:
編譯 time-timers.cpp
在 LINUX 上 gcc -O2 time-timers.cpp -o time-timers 在 Windows 2000 上 cl -O2 time-timers.cpp -o time-timers.exe |
這個程式只使用一個可選變數。預設情況下,程式調用 tend() 函數 100,000 次。重複運行該程式可保證重建時間結果。我們使用了預設計數,運行了該程式 10 次。在 Linux 2.2.16、Linux 2.4.2 和 Windows 2000 的表中分別顯示了結果。
在同一台 Thinkpad 上的 Linux 2.2.16、Linux 2.4.2 和 Windows 2000 中,我運行了以下指令碼。事實上,最初我使用了 Linux 2.4.2 的對稱式多處理 (SMP) 版本。我無意中使用了 SMP 版本進行構建和測試。發現這個情況後,我還構建了單一處理器版本並用其進行了測試。下面總結了這兩個版本的結果(如果有興趣)。
運行 running time-timers 的指令碼
ver > time-timers.out for i in 1 2 3 4 5 6 7 8 9 10 do time-timers 1m done >> time-timers.out for i in 1 2 3 4 5 6 7 8 9 10 do time-timers 1m done >> time-timers.out |
這個指令碼將把對 tend() 的一百萬次調用運行 20 遍。結果如下:
Linux 2.2.16 |
Linux 2.4.2 |
Linux 2.4.2 SMP |
Windows 2000 |
0.740 usec |
0.729 usec |
0.806 usec |
1.945 usec |
我能夠得出的唯一結論是在 Winsows 2000 裡的 QueryPerformanceCounter() 系統調用要比同一硬體上的 gettimeofday() API 慢得多。對於我們的目的來講,計時常式的 2 微秒的顆粒度是足夠的。在 1 毫秒測量時間裡,只有千分之二是實際的測量開銷。即 0.2%,對我們的目的來說是可接受的範圍。
結束語