轉載–大內高手—慣用手法

來源:互聯網
上載者:User
大內高手—慣用手法 轉載時請註明出處:http://blog.csdn.net/absurd/ 《POSA》中根據模式粒度把模式分為三類:架構模式、設計模式和慣用手法。其中把分層模式、管道過濾器和微核心模式等歸為架構模式,把代理模式、命令模式和出版-訂閱模式等歸為設計模式,而把引用計數等歸為慣用手法。這三類模式間的界限比較模糊,在特定的情況,有的設計模式可以作為架構模式來用,有的把架構模式也作為設計模式來用。 在通常情況下,我們可以說架構模式、設計模式和慣用手法,三者的重要性依次遞減,畢竟整體決策比局部決策的影響面更大。但是任何整體都是局部組成的,局部的決策也會影響全域。慣用手法的影響雖然是局部的,其作用仍然很重要。它不但在提高軟體的品質方面,而且在加快軟體開發進度方面都有很大貢獻。本文介紹幾種關於記憶體的慣用手法,這些手法對於老手來說已經習以為常,對於新手來說則是必修秘技。 1.         預分配 假想我們實現了一個動態數組(vector)時,當向其中增加元素時,它會自動擴充(縮減)緩衝區的大小,無需要調用者關心。擴充緩衝區的大小的原理都是一樣的: l         先分配一塊更大的緩衝區。l         把資料從老的緩衝區拷貝到新的緩衝區。l         釋放老的緩衝區。 如果你使用realloc來實現,記憶體管理器可能會做些最佳化:如果老的緩衝區後面有連續的空閑空間,它只需要簡單的擴充老的緩衝區,而跳過後面兩個步驟。但在大多數情況下,它都要通過上述三個步驟來完成擴充。 以此可見,擴充緩衝區對調用者來說雖然是透明的,但決不是免費的。它得付出相當大的時間代價,以及由此產生的產生記憶體片段問題。如果每次向vector中增加一個元素,都要擴充緩衝區,顯然是不太合適的。 此時我們可以採用預分配機制,每次擴充時,不是需要多大就擴充多大,而是預先分配一大塊記憶體。這一大塊可以供後面較長一段時間使用,直到把這塊記憶體全用完了,再繼續用同樣的方式擴充。 預分配機制比較常見,多見於一些帶buffer的容器實現中,比如像vector和string等。 2.         對象引用計數在物件導向的系統中,對象之間的協作關係非常複雜。所謂協作其實就調用對象的函數或者向對象發送訊息,但不管調用函數還是發送訊息,總是要通過某種方式知道目標對象才行。而最常見的做法就是儲存目標對象的引用(指標),直接引用對象而不是拷貝對象,提高了時間和空間上的效率,也避免了拷貝對象的麻煩,而且有的地方就是要對象共用才行。 對象被別人引用了,但自己可能並不知道。此時麻煩就來了,如果對象被釋放了,對該對象的引用就變成了野針,系統隨時可能因此而崩潰。不釋放也不行,因為那樣會出現記憶體泄露。怎麼辦呢? 此時我們可以採用對象引用計數,對象有一個引用計數器,不管誰要引用這個對象,就要把對象的引用計數器加1,如果不再該引用了,就把對象的引用計數器減1。當對象的引用計數器被減為0時,說明沒有其它對象引用它,該對象就可以安全的釋放了。這樣,對象的生命週期就得到了有效管理。 對象引用計數運用相當廣泛。像在COM和glib裡,都是作為對象系統的基本設施之一。即使在像JAVA和C#等現代語言中,對象引用計數也是非常重要的,它是實現記憶體回收(GC)的基本手段之一。 程式碼範例: (atlcom.h: CcomObject)
         STDMETHOD_(ULONG, AddRef)() {returnInternalAddRef();}         STDMETHOD_(ULONG, Release)()         {                   ULONGl = InternalRelease();                   if (l == 0)                            deletethis;                   returnl;         }
 3.         寫時拷貝(COW)OS核心建立子進程的過程是最常見而且最有效COW例子:建立子進程時,子進程要繼承父進程記憶體空間中的資料。但繼承之後,兩者各自有獨立的記憶體空間,修改各自的資料不會互相影響。 要做到這一點,最簡單的辦法就是直接把父進程的記憶體空間拷貝一份。這樣做可行,但問題在於拷貝內容太多,無論是時間還是空間上的開銷都讓人無法接受。況且,在大多數情況下,子進程只會使用少數繼承過來的資料,而且多數是讀取,只有少量是修改,也就說大部分拷貝的動作白做了。怎麼辦呢? 此時可以採用寫時拷貝(COW),COW代表Copy on Write。最初的拷貝只是個假象,並不是真正的拷貝,只是把引用計數加1,並設定適當的標誌。如果雙方都只是讀取這些資料,那好辦,直接讀就行了。而任何一方要修改時,為了不影響另外一方,它要把資料拷貝一份,然後修改拷貝的這一份。也就是說在修改資料時,拷貝動作才真正發生。 當然,在真正拷貝的時候,你可以選擇只拷貝修改的那一部分,或者拷貝全部資料。在上面的例子中,由於記憶體通常是按頁面來管理的,拷貝時只拷貝相關的頁面,而不是拷貝整個記憶體空間。 寫時拷貝(COW)對效能上的貢獻很大,差不多任何帶MMU的OS都會採用。當然它不限於核心空間,在使用者空間也可以使用,比如像一些String類的實現也採用了這種方法。 程式碼範例(MFC:strcore.cpp):拷貝時只是增加引用計數:
CString::CString(constCString& stringSrc){         ASSERT(stringSrc.GetData()->nRefs != 0);         if (stringSrc.GetData()->nRefs >= 0)         {                   ASSERT(stringSrc.GetData() != _afxDataNil);                   m_pchData = stringSrc.m_pchData;                   InterlockedIncrement(&GetData()->nRefs);         }         else         {                   Init();                   *this = stringSrc.m_pchData;         }}
 修改前才拷貝:
voidCString::MakeUpper(){         CopyBeforeWrite();         _tcsupr(m_pchData);} voidCString::MakeLower(){         CopyBeforeWrite();         _tcslwr(m_pchData);} 
 拷貝動作:
voidCString::CopyBeforeWrite(){         if (GetData()->nRefs > 1)         {                   CStringData* pData = GetData();                   Release();                   AllocBuffer(pData->nDataLength);                   memcpy(m_pchData, pData->data(), (pData->nDataLength+1)*sizeof(TCHAR));         }         ASSERT(GetData()->nRefs <= 1);}
  4.         固定大小分配頻繁的分配大量小塊記憶體是記憶體管理器的挑戰之一。 首先是空間利用率上的問題:由於記憶體管理本身的需要一些輔助記憶體,假設每塊記憶體需要8位元組用作輔助記憶體,那麼即使只要分配4個位元組這樣的小塊記憶體,仍然要浪費8位元組記憶體。一塊小記憶體不要緊,若存在大量小塊記憶體,所浪費的空間就可觀了。 其次是記憶體片段問題:頻繁分配大量小塊記憶體,很容易造成記憶體片段問題。這不但降低記憶體管理器的效率,同時由於這些記憶體不連續,雖然空閑卻無法使用。 此時可以採用固定大小分配,這種方式通常也叫做緩衝池(pool)分配。緩衝池(pool)先分配一塊或者多塊連續的大塊記憶體,把它們分成N塊大小相等的小塊記憶體,然後進行二次分配。由於這些小塊記憶體大小是固定的,管理大開銷非常小,往往只要一個標識位用於標識該單元是否空閑,或者甚至不需要任何標識位。另外,緩衝池(pool)中所有這些小塊記憶體分布在一塊或者幾塊串連記憶體上,所以不會有記憶體片段問題。 固定大小分配運用比較廣泛,差不多所有的記憶體管理器都用這種方法來對付小塊記憶體,比如glibc、STLPort和linux的slab等。 5.         會話緩衝池分配(Session Pool)伺服器要長時間運行,記憶體泄露是它的威脅之一,任何小機率的記憶體泄露,都可能會累積到具有破壞性的程度。從它們的運行模式來看,它們總是不斷的重複某個過程,而在這個過程中,又要分配大量(次數)記憶體。 比如像WEB伺服器,它不斷的處理HTTP請求,我們把一次HTTP請求,稱為一次會話。一次會話要經過很多階段,在這個過程要做各種處理,要多次分配記憶體。由於處理比較複雜,分配記憶體的地方又比較多,記憶體泄露可以說防不甚防。 針對這種情況,我們可以採用會話緩衝池分配。它基於 多次分配一次釋放的策略,在過程開始時建立會話緩衝池(Session Pool),這個過程中所有記憶體配置都通過會話緩衝池(Session Pool)來分配,當這個過程完成時,銷毀掉會話緩衝池(Session Pool),即釋放這個過程中所分配的全部記憶體。 因為只需要釋放一次,記憶體泄露的可能大大降低。會話緩衝池分配並不是太常見,apache採用的這種用法。後來自己用過兩次,感覺效果不錯。        當然還有其一些記憶體慣用手法,如cache等,這裡不再多說。上述部分手法在《即時設計模式》裡有詳細的描述,大家可以參考一下。        筆者水平有限,若遺漏了某些重要的記憶體慣用手法,還望各位高手補充。       ~~~end~~   Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=937803

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.