天天看點

了解new和delete的合理替換時機——條款50

        讓我們暫時回到根本原理。首先怎麼會有人想要替換編譯器提供的operator new或operator delete呢?下面是三個最常見的理由:

  • 用來檢測運用上的錯誤。如果将“new所得記憶體”delete掉卻不幸失敗,會導緻記憶體洩漏(memory leaks)。如果在“new所得記憶體”身上多次delete則會導緻不确定行為。如果operator new持有一串動态配置設定所得位址,而operator delete将位址從中移走,倒是很容易檢測出上述錯誤用法。
  • 為了強化效能。編譯器所帶的operator new和operator delete主要用于一般目的,它們不但可被長時間執行的程式(例如網頁伺服器,web servers)接受,也可被執行時間少于一秒的程式接受。它們必須處理一系列需求,包括大塊記憶體、小塊記憶體、大小混合型記憶體。它們必須接納各種配置設定形态,範圍從程式存活期間的少量區塊動态配置設定,到大數量短命對象的持續配置設定和歸還。它們必須考慮破碎問題,這最終會導緻程式無法滿足大區塊記憶體要求,即使彼時有總量足夠但分散為許多小區塊的自由記憶體。

        現實存在這麼些個對記憶體管理器的要求,是以編譯器所帶的operator news和operator deletes采取中庸之道也就不令人驚訝了。它們的工作對每個人都是适度地好,但不對特定任何人有最佳表現。如果你對你的程式的動态記憶體運用型态有深刻的了解,通常可以發現,定制版之operator new和operator delete性能勝過預設版本。說到勝過,我的意思是它們比較快,有時甚至快很多,而且它們需要的記憶體比較少,最高可省50%。對某些(雖然不是所有)應用程式而言,将舊有的(編譯器自帶的)new和delete替換為定制版本,是獲得重大效能提升的辦法之一。

  • 為了收集使用上的統計資料。在一頭栽進定制型news和定制型deletes之前,理當收集你的軟體如何使用其動态記憶體。配置設定區塊的大小分布如何?壽命分布如何?它們傾向于以FIFO(先進先出)次序或LIFO(後進先出)次序或随機次序來配置設定和歸還?它們的運用形态是否随時間改變,也就是說你的軟體在不同的執行階段有不同的配置設定/歸還形态嗎?任何時刻所使用的最大動态配置設定量(高水位)是多少?自行定義operator new和operator delete使我們得以輕松收集到這些資訊。

        觀念上,寫一個定制型operator new十分簡單。舉個例子,下面是個快速發展得出的初階段global operator new,促進并協助檢測“overruns”(寫入點在配置設定區塊尾端之後)或“underruns”(寫入點在配置設定區塊起點之前)。其中還存在不少小錯誤,稍後我會完善它。

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);  // 增加大小,使能夠塞入兩個signatures

    void* pMem = malloc(realSize);
    if (!pMem) throw bad_alloc();

    // 将signature寫入記憶體的最前段和最後段落.
    *(static_cast<int*>(pMem)) = signature;
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;
    
    // 傳回指針,指向恰位于第一個signature之後的記憶體位置.
    return static_cast<Byte*>(pMem) + sizeof(int);
}
           

        這個operator new的缺點主要在于它疏忽了身為這個特殊函數所應該具備的“堅持C++規矩”的态度。舉個例子,條款51說所有operator news都應該内含一個循環,反複調用某個new-handling函數,這裡卻沒有。由于條款51就是專門為此協定而寫,所有這兒我暫且忽略之。我現在隻想專注于一個比較微妙的主題:齊位。

        許多計算機體系結構要求特定的類型必須放在特定的記憶體位址上。例如它可能會要求指針的位址必須是4的倍數或doubles的位址必須是8的倍數。如果沒有奉行這個限制條件,可能導緻運作期硬體異常。有些體系結構比較慈悲,沒有那麼霹靂,而是宣稱如果齊位條件獲得滿足,便提供較佳效率。例如Intel x86體系結構上的doubles可被對齊于任何byte邊界,但如果它是8-byte齊位,其通路速度會快許多。

        在我們目前這個主題中,齊位意義重大,因為C++要求所有operator news傳回的指針都有适當的對齊。malloc就是這樣的要求下工作,是以令operator new傳回一個得自malloc的指針是安全的。然而上述operator new中我并未傳回一個得自malloc的指針,而是傳回一個得自malloc且偏移一個int大小的指針。沒人能夠保證它的安全!如果用戶端調用operator new企圖擷取足夠給一個double所用的記憶體(或如果我們寫個operator new[],元素類型是doubles),而我們在一部“ints為4bytes且double必須8-byte齊位”的機器上跑,我們可能會獲得一個未有适當齊位的指針。那可能會造成程式崩潰或執行速度變慢。不論哪種情況都非我們所樂見。

        本條款的主題是,了解何時在“全局性的”或“class專屬的”基礎上合理替換預設的new和delete。挖掘更多細節之前,讓我先對答案做一些摘要。

  • 為了檢測運用錯誤(如前所述)。
  • 為了收集動态記憶體配置設定之使用統計資訊(如前所述)。
  • 為了增加配置設定和歸還的速度。泛用型配置設定器往往(雖然并不總是)比定制型配置設定器慢,特别是當定制型配置設定器專門針對某特定類型之對象而設計時。
  • 為了降低預設記憶體管理器帶來的空間額外開銷。泛用型記憶體管理器往往(雖然并非總是)不隻比定制型慢,它們往往還使用更多記憶體,那是因為它們常常在每一個配置設定區塊身上招引某些額外開銷。針對小型對象而開發的配置設定器本質上消除了這樣的額外開銷。
  • 為了彌補預設配置設定器中的非最佳齊位。一如先前所說,在x86體系結構上doubles的通路最是快速——如果它們都是8-byte齊位。但是編譯器自帶的operator news并不保證對動态配置設定而得的doubles采取8-byte齊位。這種情況下,将預設的operator new替換為一個8-byte齊位保證版,可導緻程式效率大幅提升。
  • 為了将相關對象成簇集中。如果你知道特定之某個資料結構往往被一起使用,而你又希望在處理這些資料時将“記憶體頁錯誤”的頻率降至最低,那麼為此資料結建構立另一個heap就有意義,這麼一來它們就可以被成簇集中在盡可能少的記憶體頁上。new和delete的“placement版本”(見條款52)有可能完成這樣的集簇行為。
  • 為了獲得非傳統的行為。有時候你會希望operators new和delete做編譯器附帶版沒做的某些事情。例如你可能會希望配置設定和歸還共享記憶體内的區塊,但唯一能夠管理該記憶體的隻有C API函數,那麼寫下一個定制版new和delete,你便得以為C API穿上一件C++外套。你也可以寫一個自定的operator delete,在其中将所有歸還的記憶體内容覆寫為0,籍此增加應用程式的資料安全性。

請記住

  • 有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行調試、收集heap使用資訊。

繼續閱讀