1. 程式人生 > >Open Cascade中的記憶體管理

Open Cascade中的記憶體管理

Open Cascade中的記憶體管理

Memory Management in Open Cascade

一、C++中的記憶體管理 Memory Management in C++

1. 引言

為了表現出多型,在C++中就會用到大量的指標和引用。指標所指的物件是從記憶體空間中借來的,當然要及時歸還。特別是指標在程式中隨心所欲地建立,因此,一個指標究竟指向哪個物件,一個物件到底被幾個指標所指向,是程式設計師十分關注的事情。

C++中涉及到的記憶體管理問題可以歸結為兩方面:正確地掌握它和有效地使用它。好的程式設計師會理解這兩個問題為什麼要以這樣的順序列出。因為執行得再快、體積再小的程式,如果不按所期望的方式去執行也是沒什麼用處的程式。對於大多數程式設計師,正確地掌握意味著正確地呼叫記憶體分配和釋放函式;有效地使用意味著編寫自定義版本的記憶體分配和釋放函式。顯然,正確地掌握它要重要些。

C中,只要用malloc分配的記憶體沒有用free釋放就會產生記憶體洩露。在C++中肇事者的名字換成了newdelete,但是問題依然存在。當然,有了解構函式情況稍有改觀。因為解構函式為所有將被銷燬的物件提供了一個方便的呼叫delete的場所,但這同時又帶來了更多的煩惱,因為newdelete是隱式地呼叫建構函式和解構函式的。而且可以在類中和類外自定義newdelete操作符,這又帶來了複雜性,增加出錯的機會。

2. 記憶體分配方式

記憶體分配有三種方式:

u 從靜態儲存區域分配。記憶體在編譯時就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數、static變數;

u 從棧上分配。在執行函式時,函式內的區域性變數的儲存單元都能在棧上建立,函式執行結束時,這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配記憶體容量有限;

u 從堆上分配,亦稱動態記憶體分配。程式在執行時用mallocnew申請任意多少的記憶體,程式設計師自己負責在用完時使用freedelete來釋放記憶體。動態記憶體的生存期由我們決定,使用起來很靈活,但問題也最多。

二、Open Cascade中的記憶體管理 Memory Management in Open Cascade

在幾何建模的過程中,程式建立和刪除了大量的物件在動態記憶體中,也就是堆中。在這種情況下,標準C++的記憶體管理方式不是很高效,所以Open Cascade在包Standard中專門寫了個記憶體管理程式(Memory Manager)來對記憶體的分配與刪除進行管理。

1. 用法 Usage

為了在C程式碼中使用Open Cascade提供的記憶體管理器,只需要將原來使用malloc的地方使用Standard::Allocate來代替,原來使用free的地方使用Standard::Free來代替。另外,原來使用realloc的地方使用Standard::Reallocate來代替即可。

C++中,operator newdelete都重新定義以便使用Open Cascade的記憶體管理器。定義程式碼如下所示:

public:
  // Redefined operators new and delete ensure that handles are 
  // allocated using OCC memory manager
  void* operator new(size_t,void* anAddress) 
  {
    return anAddress;
  }
  void* operator new(size_t size) 
  { 
    return Standard::Allocate(size); 
  }
  void  operator delete(void *anAddress, size_t ) 
  { 
    if (anAddress) Standard::Free(anAddress); 
  }

上述程式碼是將operator newdeleteplacement new都重新定義了,這樣的類的newdelete都將由Open Cascade的記憶體管理器來管理。

CDL extractor為在其中所有類都採用這種方式來重新定義operator newdelete,這樣Open Cascade所有的類(少數除外)都是使用Open Cascade的記憶體管理器來管理。

2. 配置記憶體管理器 Configuring memory manager

Open CASCADE記憶體管理器可以配置,按不同的優化方式來分配記憶體,主要還是看需要分配記憶體的大小,或者不使用記憶體優化而直接使用mallocfree

配置方式為設定如下環境變數的值:

l MMGT_OPT:若設定為1(預設值也是為1),記憶體管理器將使用記憶體優化的方式來管理記憶體;若設定為0,則記憶體的分配就是直接呼叫C的函式mallocfree來對記憶體進行管理,此時,所有其它選項除了MMGT_CLEAR外都將被忽略。若設定為2,則會使用IntelTBB來對記憶體的分配進行優化,此時需要有TBB的庫。

l MMGT_CLEAR:若設定為1(預設值也是為1),分配的記憶體塊將被清零;若設定為0,則記憶體塊將以分配時的值返回。

l MMGT_CELLSIZE:定義了記憶體池中可分配記憶體塊的最大值。預設值為200

l MMGT_NBPAGES:定義了頁面上可分配的小的記憶體塊的數量,預設值為1000

l MMGT_THRESHOLD:定義了迴圈利用的而不是返回給堆的記憶體塊的數量,預設值為4000

l MMGT_MMAP:若設定為1(預設值也是為1),大記憶體塊的分配將會使用作業系統的記憶體對映函式。若設定為0,記憶體的分配將會直接使用malloc直接在堆上分配。

l MMGT_REENTRANT:若設定為1(預設值為0),所有呼叫記憶體優化的函式將會被保證安全,即使有多個不同的執行緒。當在使用記憶體優化管理(MMGT_OPT=1)記憶體及多執行緒的程式時,這個值需要設定為1

注:為了使用Open Cascade在多執行緒的程式中表現出更好的效能,推薦如下兩種設定方式:

l MMGT_OPT=0

l MMGT_OPT=1 and MMGT_REENTRANT=1

3. 程式實現 Implementation details

Standard_MMgrRoot為記憶體管理器的抽象類,它定義了記憶體分配的釋放的虛擬函式。通過環境變數MMGT_OPT來選擇不同的記憶體管理類,如下程式碼所示:

Standard_MMgrFactory::Standard_MMgrFactory() : myFMMgr(0)
{
  char *var;
  Standard_Boolean bClear, bMMap, bReentrant;
  Standard_Integer aCellSize, aNbPages, aThreshold, bOptAlloc;
  //
  bOptAlloc   = atoi((var = getenv("MMGT_OPT"      )) ? var : "1"    ); 
  bClear      = atoi((var = getenv("MMGT_CLEAR"    )) ? var : "1"    );
  bMMap       = atoi((var = getenv("MMGT_MMAP"     )) ? var : "1"    ); 
  aCellSize   = atoi((var = getenv("MMGT_CELLSIZE" )) ? var : "200"  ); 
  aNbPages    = atoi((var = getenv("MMGT_NBPAGES"  )) ? var : "1000" );
  aThreshold  = atoi((var = getenv("MMGT_THRESHOLD")) ? var : "40000");
  bReentrant  = atoi((var = getenv("MMGT_REENTRANT")) ? var : "0"    );
  if ( bOptAlloc == 1 ) { 
    myFMMgr = new Standard_MMgrOpt(bClear, bMMap, aCellSize, aNbPages,
                                   aThreshold, bReentrant);
  }
  else if ( bOptAlloc == 2 ) {
    myFMMgr = new Standard_MMgrTBBalloc(bClear);
  }
  else {
    myFMMgr = new Standard_MMgrRaw(bClear);
  }
  // Set grobal reentrant flag according to MMGT_REENTRANT environment variable
  if ( ! Standard_IsReentrant )
    Standard_IsReentrant = bReentrant;
}

MMGT_OPT設定為1時,將會使用類Standard_MMgrOpt來對記憶體的分配與釋放進行優化。優化方法如下:

l 小型記憶體塊(小於MMGT_CELLSIZE的記憶體)不是單獨分配。而是分配一個大的記憶體池(每個記憶體池的大小是MMGT_NBPAGES),每個新建記憶體都被安排在當前的記憶體池中空閒的地方。若當前記憶體池被佔滿,則分配另一個記憶體池。在當前的版本中,記憶體池不會返回給系統(直到程式結束)。然而,呼叫函式Standard::Free()被釋放的記憶體塊會被free列表記錄,以便在下一個相同大小的記憶體塊分配時重新利用(迴圈使用)。

l 中型記憶體塊(大小在MMGT_CELLSIZEMMGT_THRESHOLD之間的記憶體塊)由C的函式mallocfree直接管理。當這樣的記憶體塊被呼叫函式Standard::Free釋放時,它們也像小型記憶體塊那樣被迴圈使用。與小型記憶體塊不同的是,被釋放的free列表中包含的中型記憶體塊可以通過函式Standard::Purge,使其返回到堆中。

l 大型記憶體塊(大於MMGT_THRESHOLD的記憶體塊,包含用於管理小型記憶體塊的記憶體池)的分配取決於MMGT_MMAP的值:若為0,這些記憶體塊在堆中分配;否則,將會使用作業系統的專用的管理記憶體對映檔案的函式來分配。當使用Standard::Free來釋放大型記憶體塊時,大型記憶體塊立即返回給系統。

4. 利與弊 Benefits and drawbacks

Open Cascade使用記憶體管理器的最大好處就是其對小型記憶體塊的迴圈使用機制。當程式需要對大量小型記憶體塊進行分配與釋放時,這種機制使程式速度更快。實踐表明,使用這種方式程式的效能可以提高50%以上。

相應的弊端就是迴圈使的記憶體在程式執行時不會返回給系統。這就可能導致大量的記憶體消耗,甚至可能導致記憶體洩露。為了避免這種情況,應該在大量使記憶體的操作結束後呼叫函式Standard::Purge

使用Open Cascade的記憶體管理器(Memory Manager)導致的所有的記憶體開銷有:

l 分配的每個記憶體塊的大小都會以8個位元組向上取整。(看其原始碼應該是以的個位元組向上取整,源程式如下所示:)

Standard_Address Standard_MMgrRaw::Allocate(const Standard_Size aSize)
{
  // the size is rounded up to 4 since some OCC classes
  // (e.g. TCollection_AsciiString) assume memory to be double word-aligned
  const Standard_Size aRoundSize = (aSize + 3) & ~0x3;
  // we use ?: operator instead of if() since it is faster :-)
  Standard_Address aPtr = ( myClear ? calloc(aRoundSize, sizeof(char)) :
                                      malloc(aRoundSize) );
  if ( ! aPtr )
    Standard_OutOfMemory::Raise("Standard_MMgrRaw::Allocate(): malloc failed");
  return aPtr;
}

l 額外的4個位元組(在64位的作業系統上是8個位元組)將在每個記憶體塊的開始部分分配,用來儲存其大小(或用來儲存下一個可用的記憶體塊的地址),只在MMGT_OPT1時有效。

所以不管Open Cascade的記憶體管理器以優化方式還是標準方式來管理記憶體,記憶體總的消耗都將會大一些。