VC++多執行緒下記憶體操作的最佳化 (轉)

worldblog發表於2007-12-02
VC++多執行緒下記憶體操作的最佳化 (轉)[@more@]

作者/李紅亞:namespace prefix = o ns = "urn:schemas--com::office" />

 

許多員發現用VC++編寫的程式在多的上執行會變得很慢,這種情況多是由於多個執行緒爭用同一個資源引起的。對於用VC++編寫的程式,問題出在VC++的管理的具體實現上。以下透過對這個問題的解釋,提供一個簡便的解決方法,使得這種程式在多處理器下避免出現執行瓶頸。這種方法在沒有VC++程式的時也能用。

問題

  C和C++執行庫提供了對於堆記憶體進行管理的:C提供的是malloc()和free()、C++提供的是new和delete。無論是透過malloc()還是new申請記憶體,這些函式都是在堆記憶體中尋找一個未用的塊,並且塊的大小要大於所申請的大小。如果沒有足夠大的未用的記憶體塊,執行時間庫就會向操作請求新的頁。頁是虛擬記憶體管理器進行操作的單位,在基於的處理器的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_()函式內部分以下三個步驟:

  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:bingyapp.exe,2

 

測試

  為了驗證在多處理器下的效果,編了一個測試程式heaptest.c。該程式接收三個引數,第一個參數列示執行緒數,第二個引數是所申請的記憶體的最大值,第三個引數每個執行緒申請記憶體的次數。

#define _LEAN_AND_MEAN

#include <.h>

#include

#include

#include

/* 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 traners 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 allocCountnn",

  argv[0]);

  exit(1);

}

unsigned __stdcall workerThread(void* myThreadData)

{

  int count;

  threadData* myData;

  char* dummy;

  srand(GetTickCount()*GetCurrentThreadId());

  myData=(threadData*)myThreadData;

  /* now let us do the real work */

  for(count=0;countallocCount;count++)

  {

  dummy=(char*)malloc((rand()%myData->maximumLength)+1);

  free(dummy);

  }

  _endthreadex(0);

  /* to satisfy compiler */

  return 0;

}

int main(int argc,char** argv)

{

  int threadCount;

  int count;

  threadData actData;

  HANDLE* threadHandles;

  D startTime;

  DWORD stopTime;

  DWORD retValue;

  unsigned dummy;

  /* check parameters */

  if(argc<4 || argc>4)

  printUsage(argv);

  /* get parameters for this run */

  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

  {

  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=WaitForMultiples(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

  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++程式,問題通常發生在當多個執行緒進行頻繁的記憶體操作活動時。如上文所述,只要進行很少的一些設定,就可能極大地提高多執行緒程式在多處理器下的效能。這種方法即不需要源程式,也不需要重新編譯可執行檔案,而最大的好處是用這種方法得到的效能的提高是不用支付任何費用的。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-987423/,如需轉載,請註明出處,否則將追究法律責任。

相關文章