天天看點

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;

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