149
技術社區[雲棲]
VC++多線程下內存操作的優化
許多程序員發現用VC++編寫的程序在多處理器的電腦上運行會變得很慢,這種情況多是由 於多個線程爭用同一個資源引起的。對於用VC++編寫的程序,問題出在VC++的內存管理的具體實現上。以下通過對這個問題的解釋,提供一個簡便的解決方 法,使得這種程序在多處理器下避免出現運行瓶頸。這種方法在沒有VC++程序的源代碼時也能用。
問題
C和C++運行庫提供了對於堆內存進行管理的函數:C提供的是malloc()和free()、C++提供的是new和delete。無論是通過 malloc()還是new申請內存,這些函數都是在堆內存中尋找一個未用的塊,並且塊的大小要大於所申請的大小。如果沒有足夠大的未用的內存塊,運行時 間庫就會向操作係統請求新的頁。頁是虛擬內存管理器進行操作的單位,在基於Intel的處理器的NT平台下,一般是4,096字節。當你調用free() 或delete釋放內存時,這些內存塊就返還給堆,供以後申請內存時用。
這些操作看起來不太起眼,但是問題的關鍵。問題就發生在當多個線程幾乎同申請內存時,這通常發生在多處理器的係統上。但即使在一個單處理器的係統上,如果線程在錯誤的時間被調度,也可能發生這個問題。
考慮處於同一進程中的兩個線程,線程1在申請1,024字節的內存的同時,運行於另外一個處理器的線程2申請256字節內存。內存管理器發現一個未 用的內存塊用於線程1,同時同一個函數發現了同一塊內存用於線程2。如果兩個線程同時更新內部數據結構,記錄所申請的內存及其大小,堆內存就會產生衝突。 即使申請內存的函數者成功返回,兩個線程都確信自己擁有那塊內存,這個程序也會產生錯誤,這隻是個時間問題。
產生這種情況稱為爭用,是編寫多線程程序的最大問題。解決這個問題的關鍵是要用一個鎖定機製來保護內存管理器的這些函數,鎖定機製保證運行相同代碼的多個線程互斥地進行,如果一個線程正運行受保護的代碼,則其他的線程都必須等待,這種解決方法也稱作序列化。
NT提供了一些鎖定機製的實現方法。CreateMutex()創建一個係統範圍的鎖定對象,但這種方法的效率最低; InitializeCriticalSection()創建的critical section相對效率就要高許多;要得到更好的性能,可以用具有service pack 3的NT 4的spin lock,更詳細的信息可以參考VC++幫助中的InitializeCriticalSectionAndSpinCount()函數的說明。有趣的 是,雖然幫助文件中說spin lock用於NT的堆管理器(HeapAlloc()係列的函數),VC++運行庫的堆管理函數並沒有用spin lock來同步對堆的存取。如果查看VC++運行庫的堆管理函數的源程序,會發現是用一個critical section用於全部的內存操作。如果可以在VC++運行庫中用HeapAlloc(),而不是其自己的堆管理函數,將會因為使用的是spin lock而不是critical section而得到速度優化。
通過使用critical section同步對堆的存取,VC++運行庫可以安全地讓多個線程申請和釋放內存。然而,由於內存的爭用,這種方法會引起性能的下降。如果一個線程存取 另外一個線程正在使用的堆時,前一個線程就需要等待,並喪失自己的時間片,切換到其他的線程。線程的切換在NT下是相當費時的,因為其占用線程的時間片的 一個小的百分比。如果有多個線程同時要存取同一個堆,會引起更多的線程切換,足夠引起極大的性能損失。
現象
如何發現多處理器係統存在這種性能損失?有一個簡便的方法,打開“管理工具”中的“性能”監視器,在係統組中添加一個上下文切換/秒計數,然後運行 想要測試的多線程程序,並且在進程組中添加該進程的處理器時間計數,這樣就可以得到處理器在高負荷下要發生多少次上下文切換。
在高負荷下有上千次的上下文切換是正常的,但當計數超過80,000或100,000時,說明過多的時間都浪費在線程的切換,稍微計算一下就可以知 道,如果每秒有100,000次線程切換,則每個線程隻有10微秒用於運行,而NT上的正常的時間片長度約有12毫秒,是前者的上千倍。
性能圖顯示了過度的線程切換,而圖2顯示了同一個進程在同樣的環境下,在使用了下麵提供的解決方法後的情況。係統每秒鍾要進行120,000次線程 切換,改進後,每秒鍾線程切換的次數減少到1,000次以下。兩張圖都是在運行同一個測試程序時截取得,程序中同時有3個線程同時進行最大為2,048字 節的堆的申請,硬件平台是一個雙Pentium II 450機器,有256MB內存。
解決方法
本方法要求多線程程序是用VC++編寫的,並且是動態鏈接到C運行庫的。要求NT係統所安裝的VC++運行庫文件msvcrt.dll的版本號是 6,所安裝的service pack的版本是5以上。如果程序是用VC++ v6.0以上版本編譯的,即使多線程程序和libcmt.lib是靜態鏈接,本方法也可以使用。
當一個VC++程序運行時,C運行庫被初始化,其中一項工作是確定要使用的堆管理器,VC++ v6.0運行庫既可以使用其自己內部的堆管理函數,也可以直接調用操作係統的堆管理函數(HeapAlloc()係列的函數),在 __heap_select()函數內部分執行以下三個步驟:
1、檢查操作係統的版本,如果運行於NT,並且主版本是5或更高(Window 2000及以後版本),就使用HeapAlloc()。
2、查找環境變量__MSVCRT_HEAP_SELECT,如果有,將確定使用哪個堆函數。如果其值是 __GLOBAL_HEAP_SELECTED,則會改變所有程序的行為。如果是一個可執行文件的完整路徑,還要調用GetModuleFileName ()檢查是否該程序存在,至於要選擇哪個堆函數還要查看逗號後麵的值,1表示使用HeapAlloc(),2表示使用VC++ v5的堆函數,3表示使用VC++ v6的堆函數。
3、檢測可執行文件中的鏈接程序標誌,如果是由VC++ v6或更高的版本創建的,就使用版本6的堆函數,否則使用版本5的堆函數。
那麼如何提高程序的性能?如果是和msvcrt.dll動態鏈接的,保證這個dll是1999年2月以後,並且安裝的service pack的版本是5或更高。如果是靜態鏈接的,保證鏈接程序的版本號是6或更高,可以用quickview.exe程序檢查這個版本號。要改變所要運行的 程序的堆函數的選取,在命令行下鍵入以下命令:
- set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1
以後,所有從這個命令行運行的程序,都會繼承這個環境變量的設置。這樣,在堆操作時都會使用HeapAlloc()。如果讓所有的程序都使用這些速 度更快的堆操作函數,運行控製麵板的“係統”程序,選擇“環境”,點取“係統變量”,輸入變量名和值,然後按“應用”按鈕關閉對話框,重新啟動機器。
按照微軟的說法,可能有一些用VC++ v6以前版本編譯程序,使用VC++ v6的堆管理器會出現一些問題。如果在進行以上設置後遇到這樣的問題,可以用一個批處理文件專門為這個程序把這個設置去掉,例如:
- set __MSVCRT_HEAP_SELECT=c:/program files/myapp/myapp.exe,1 c:/bin/buggyapp.exe,2
測試
為了驗證在多處理器下的效果,編了一個測試程序heaptest.c。該程序接收三個參數,第一個參數表示線程數,第二個參數是所申請的內存的最大值,第三個參數每個線程申請內存的次數。
- #define WIN32_LEAN_AND_MEAN
- #include <windows.h>
- #include <process.h>
- #include <stdio.h>
- #include <stdlib.h>
- // compile with cl /MT heaptest.c
- /* to switch to the system heap issue the following command
- before starting heaptest from the same command line
- set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */
- //structure transfers variables to the worker threads
- typedef struct tData {
- int maximumLength;
- int allocCount;
- } threadData;
- void printUsage(char** argv) {
- fprintf(stderr,"Wrong number of parameters./nUsage:/n");
- fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n", argv[0]);
- exit(1);
- }
- unsigned __stdcall workerThread(void* myThreadData) {
- int count;
- threadData* myData;
- char* dummy;
- srand(GetTickCount()*GetCurrentThreadId());
- //now let us do the real work
- myData=(threadData*)myThreadData;
- for (count=0;countallocCount;count++) {
- dummy=(char*)malloc((rand()%myData->maximumLength)+1);
- free(dummy);
- }
- //to satisfy compiler/
- _endthreadex(0);
- return 0;
- }
- int main(int argc,char** argv) {
- int threadCount;
- int count;
- threadData actData;
- HANDLE* threadHandles;
- DWORD startTime;
- DWORD stopTime;
- DWORD retValue;
- // check parameters
- unsigned dummy;
- // get parameters for this run
- if (argc<4 || argc>4) printUsage(argv);
- threadCount=atoi(argv[1]);
- if (threadCount>64) threadCount=64;
- actData.maximumLength=atoi(argv[2])-1;
- actData.allocCount=atoi(argv[3]);
- threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));
- printf("Test run with %d simultaneous threads:/n",threadCount);
- startTime=GetTickCount();
- for(count=0;count<threadCount;count++)
- {
- threadHandles[count]=(HANDLE)_beginthreadex(0,0,
- &workerThread, (void*)&actData,0,&dummy);
- if (threadHandles[count]==(HANDLE)-1)
- {
- fprintf(stderr,"Error starting worker threads./n");
- exit(2);
- }
- }
- // wait until all threads are done
- retValue=WaitForMultipleObjects(threadCount,threadHandles,
- 1,INFINITE);
- stopTime=GetTickCount();
- printf("Total time elapsed was: %d milliseconds",
- stopTime-startTime);
- printf(" for %d alloc operations./n",
- actData.allocCount*threadCount);
- // cleanup
- for(count=0;count<threadCount;count++)
- CloseHandle(threadHandles[count]);
- free(threadHandles);
- return 0;
- }
測試程序在處理完參數後,創建參數1指定數量的線程,threadData結構用於傳遞計數變量。workThread中進行內存操作,首先初始化 隨機數發生器,然後進行指定數量的malloc()和free()操作。主線程調用WaitForMultipleObject()等待工作者線程結束, 然後輸出線程運行的時間。計時不是十分精確,但影響不大。
為了編譯這個程序,需要已經安裝VC++ v6.0程序,打開一個命令行窗口,鍵入以下命令:
- cl /MT heaptest.c
/MT表示同C運行庫的多線程版靜態鏈接。如果要動態鏈接,用/MD。如果VC++是v5.0的話並且有高版本的msvcrt.dll,應該用動態鏈接。 現在運行這個程序,用性能監視器查看線程切換的次數,然後按上麵設置環境參數,重新運行這個程序,再次查看線程切換次數。
當截取這兩張圖時,測試程序用了60,953ms進行了3,000,000次的內存申請操作,使用的是VC++ v6的堆操作函數。在轉換使用HeapAlloc()後,同樣的操作僅用了5,291ms。在這個特定的情況下,使用HeapAlloc()使得性能提高 了10倍以上!在實際的程序同樣可以看到這種性能的提升。
結論
多處理器係統可以自然提升程序的性能,但如果發生多個處理器爭用同一個資源,則可能多處理器的係統的性能還不如單處理器係統。對於C/C++程序,問題通 常發生在當多個線程進行頻繁的內存操作活動時。如上文所述,隻要進行很少的一些設置,就可能極大地提高多線程程序在多處理器下的性能。這種方法即不需要源 程序,也不需要重新編譯可執行文件,而最大的好處是用這種方法得到的性能的提高是不用支付任何費用的。
最後更新:2017-04-02 00:06:17