標籤:c++ 記憶體 delete 高效c++
有人會想要替換掉編譯器提供的operator new或operator delete,因為
- 用來檢測運用上的錯誤。如果delete new的記憶體失敗,會導致記憶體流失。如果在new所得記憶體多次delete會導致不確定行為。使用編譯器提供的operator new和operator delete不能檢測上述行為。如果operator new持有一個鏈表,其儲存動態分配所得記憶體,operator delete則將記憶體從鏈表刪除,這樣就能呢檢測上述錯誤用法。如果編程錯誤,可能在分配記憶體的之前地區或之後地區寫入資料;這時可以自己定義operator new分配超額記憶體,在多出部分寫上特定byte patterns(即簽名,signature),自己定義operator delete檢測簽名是否更改。
- 為了強化效能。operator new和operator delete如果開闢大記憶體、小記憶體,持續這樣做會造成記憶體片段,這在伺服器的背景程式上,可能會導致無法滿足大區快記憶體需求,即使有足夠但分散的小區塊自由記憶體。使用自己定製的operator new和operator delete可以避免這樣的問題。針對特定的需求,有時還可以提升效能。
- 為收集使用上的統計資料。在定製operator new和operator delete之前,應該首先瞭解軟體如何使用動態記憶體。分配區塊如何分布?壽命如何?它們是FIFO先進先出還是LIFO後進先出,或隨機分配和歸還?軟體在不同執行階段有不同的分配歸還形態嗎?任何時刻使用的最大動態分配量是多少?自己定義的operator new和operator delete可以輕鬆收集到這些資訊。
寫個定製的operator new和operator delete並不難。例如,寫個global operator new,用於檢測在分配區塊的後面或前面寫入資料。下面是個初步版本,有小錯誤,後面在完善。
static const int signature=0xDEADBEEF;typedef unsigned char Byte;//下面代碼有些小錯誤void* operator new(std::size_t size) throw(std::bad_alloc){ using namespace std; size_t realSize=size+2*sizeof(int);//增加大小,塞入兩個sinature void* pMem=malloc(realSize); if(!pMem) throw bad_alloc(); //將signarure寫入記憶體最前後最後 *(static_cast<int*>(pMem))=signarure; *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int)))=signature; return static_cast<Byte*>(pMem)+sizeof(int);}
暫且忽略之前所說的operator new內應該有個迴圈,反覆調用new-handling。來說一下另外一個主題:對齊(alignment)。
許多電腦體繫結構要求特定的類型必須放在特定的記憶體位址上。例如可能是指標的地址必須是4的倍數(four-byte aligned)或double的地址是8的倍數(eight-byte aligned)。沒有這些約束可能會導致運行期硬體異常。有些體繫結構要求沒這麼嚴格,沒有位元組對齊不會導運行效率低下。
C++要求所有operator new返回的指標都有適當的對齊(取決於資料類型)。malloc就是在這樣的要求下工作。所以令operator new返回一個得自malloc的指標是安全的。但是上面實現中,我們位移了一個int的大小,就不能保證其安全了。例如,如果返回double指標,就不是8位元組對齊了。
像對齊這類技術細節,可以區分記憶體管理器的品質。寫一個能夠啟動並執行記憶體管理器並不難,難的是讓它總是能夠高效優良的運作。一般來說,若非必要,不要去寫記憶體管理器。
很多時候也是非必要的。有些編譯器已經在它們的記憶體管理函數中切換至調試狀態(enable debugging)和志記狀態(logging)。許多平台上有商業產品可以代替編譯器內建的記憶體管理器,可以用它們來提高機能和改善效率。
另外一個選擇是開源領域中的記憶體管理器。它們對許多平台都可以用。Boost程式庫(條款 55)的Pool就是這樣的一個分配器,它對常見的分配大量小記憶體很有協助。一些小型開源記憶體 Clerk大多都不完整,缺少移植、安全執行緒、對齊等考慮。
本條款是在探討何時需要在全域性的活class專屬的基礎上合理替換掉預設的new和delete,前面說到了3點。這裡繼續。
- 為了增加分配和歸還的速度。使用定製的針對特定類型對象的分配器,可以提高效率。例如,Boost提供的Pool程式庫便是。如果在單線程程式中,你的編譯器所帶的記憶體管理具備安全執行緒,你可以寫個不具備安全執行緒的分配器而大幅度改善速度。
- 為了降低預設記憶體管理器帶來的空間額外開銷。泛用型分配器往往(雖然並非總是)不只比定製型慢,還使用更多空間,因為它們常常在每一個分配區塊上招引某些額外開銷。針對小型對象開放的分配器,例如Boost庫的Pool,本質上消除了這樣的額外開銷。
- 為了彌補預設分配器的非最佳對齊(suboptimal alignment)。X86體繫結構上的double訪問最快–如果它們是8-byte對齊。但是編譯器內建的operator new並不保證分配double是8-byte對齊。
- 為了將相關對象成簇集中。如果特定的某個資料結構往往被一起使用,我們希望在處理這些資料時將“記憶體頁錯誤”(page faults)的頻率降至最低,那麼為此資料結構建立另一個heap就有意義,這樣就可以將它們成簇集中到儘可能少的記憶體也上。
- 為了獲得非傳統的行為。有時候我們需要做operator new和delete沒做的事。例如,在歸還記憶體時將其資料覆蓋為0,以此增加應用程式的資料安全。
總結
- 有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行調試、收集heap使用資訊。
《Effective C++》:條款50:瞭解new和delete的合理替換時機