許多程序員發現用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毫秒,是前者的上千倍。
圖1的性能圖顯示了過度的線程切換,而圖2顯示了同一個進程在同樣的環境下,在使用了下面提供的解決方法後的情況。圖1的情況下,系統每秒鐘要進行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 filesmyappmyapp.exe,1 c:inuggyapp.exe,2
測試
為了驗證在多處理器下的效果,編了一個測試程序heaptest.c。該程序接收三個參數,第一個參數表示線程數,第二個參數是所申請的內存的最大值,第三個參數每個線程申請內存的次數。
#define WIN32_LEAN_AND_MEAN
#inclu
[1] [2] [3] 下一頁