Boost庫是一個可移植的開源C++函數庫,鑒于STL(標準模板庫)已經成為C++語言的一個組成部分,可以毫不夸張的說,Boost是目前影響最大的通用C++庫。Boost庫由C++標準委員會庫工作組成員發起,其中有些內容有望成為下一代C++標準庫內容,是一個“準”標準庫。
Boost內存池,即boost.pool庫,是由Boost提供的一個用于內存池管理的開源C++庫。作為Boost中影響較大的一個庫,Pool已經被廣泛使用。
1. 什么是內存池
“池”是在計算機技術中經常使用的一種設計模式,其內涵在于:將程序中需要經常使用的核心資源先申請出來,放到一個池內,由程序自己管理,這樣可以提高資源的使用效率,也可以保證本程序占有的資源數量。經常使用的池技術包括內存池、線程池和連接池等,其中尤以內存池和線程池使用最多。
內存池(Memory Pool)是一種動態內存分配與管理技術。通常情況下,程序員習慣直接使用new、delete、malloc、free等API申請分配和釋放內存,導致的后果時:當程序長時間運行時,由于所申請內存塊的大小不定,頻繁使用時會造成大量的內存碎片從而降低程序和操作系統的性能。內存池則是在真正使用內存之前,先申請分配一大塊內存(內存池)留作備用,當程序員申請內存時,從池中取出一塊動態分配,當程序員釋放內存時,將釋放的內存再放入池內,并盡量與周邊的空閑內存塊合并。若內存池不夠時,則自動擴大內存池,從操作系統中申請更大的內存池。
內存池的應用場景
早期的內存池技術是為了專門解決那種頻繁申請和釋放相同大小內存塊的程序,因此早期的一些內存池都是用相同大小的內存塊鏈表組織起來的。
Boost的內存池則對內存塊的大小是否相同沒有限制,因此只要是頻繁動態申請釋放內存的長時間運行程序,都適用Boost內存池。這樣可以有效減少內存碎片并提高程序運行效率。
安裝
Boost的pool庫是以C++頭文件的形式提供的,不需要安裝,也沒有lib或者dll文件,僅僅需要將頭文件包含到你的C++工程中就可以了。Boost的最新版本可以到http://www.boost.org/下載。
2. 內存池的特征
2.1 無內存泄露
正確的使用內存池的申請和釋放函數不會造成內存泄露,更重要的是,即使不正確的使用了申請和釋放函數,內存池中的內存也會在進程結束時被全部自動釋放,不會造成系統的內存泄露。
2.2 申請的內存數組沒有被填充
例如一個元素的內存大小為A,那么元素數組若包含n個元素,則該數組的內存大小必然是A*n,不會有多余的內存來填充該數組。盡管每個元素也許包含一些填充的東西。
2.3 任何數組內存塊的位置都和使用operator new[]分配的內存塊位置一致
這表明你仍可以使用那些通過數組指針計算內存塊位置的算法。
2.4 內存池要比直接使用系統的動態內存分配快
這個快是概率意義上的,不是每個時刻,每種內存池都比直接使用new或者malloc快。例如,當程序使用內存池時內存池恰好處于已經滿了的狀態,那么這次內存申請會導致內存池自我擴充,肯定比直接new一塊內存要慢。但在大部分時候,內存池要比new或者malloc快很多。
3. 內存池效率測試
3.1 測試1:連續申請和連續釋放
分別用內存池和new連續申請和連續釋放大量的內存塊,比較其運行速度,代碼如下:
測試環境:VS2008,WindowXP SP2,Pentium 4 CPU雙核,1.5GB內存。

結論:在連續申請和連續釋放10萬塊內存的情況下,使用內存池耗時是使用new耗時的47.46%。
3.2 測試2:反復申請和釋放小塊內存
代碼如下:
測試結果如下:

結論:在反復申請和釋放50萬次內存的情況下,使用內存池耗時是使用new耗時的64.34%。
3.3 測試3:反復申請和釋放C++對象
C++對象在動態申請和釋放時,不僅要進行內存操作,同時還要調用構造和析購函數。因此有必要對C++對象也進行內存池的測試。
代碼如下:
測試結果如下:

結論:在反復申請和釋放50萬個C++對象的情況下,使用內存池耗時是使用new耗時的112.03%。這是因為內存池的construct和destroy函數增加了函數調用次數的原因。這種情況下使用內存池并不能獲得性能上的優化。
4. Boost內存池的分類
Boost內存池按照不同的理念分為四類。主要是兩種理念的不同造成了這樣的分類。
一是Object Usage和Singleton Usage的不同。Object Usage意味著每個內存池都是一個可以創建和銷毀的對象,一旦內存池被銷毀則其所分配的所有內存都會被釋放。Singleton Usage意味著每個內存池都是一個被靜態分配的對象,直至程序結束才會被銷毀,這也意味著這樣的內存池是多線程安全的。只有使用release_memory或者 purge_memory方法才能釋放內存。
二是內存溢出的處理方式。第一種方式是返回NULL代表內存池溢出了;第二種方式是拋出異常代表內存池溢出。
根據以上的理念,boost的內存池分為四種。
4.1 Pool
Pool是一個Object Usage的內存池,溢出時返回NULL。
4.2 object_pool
object_pool與pool類似,唯一的區別是當其分配的內存釋放時,它會嘗試調用該對象的析購函數。
4.3 singleton_pool
singleton_pool是一個Singleton Usage的內存池,溢出時返回NULL。
4.4 pool_alloc
pool_alloc是一個Singleton Usage的內存池,溢出時拋出異常。
5. 內存池溢出的原理與解決方法
5.1 必然溢出的內存
內存池簡化了很多內存方面的操作,也避免了一些錯誤使用內存對程序造成的損害。但是,使用內存池時最需要注意的一點是要處理內存池溢出的情況。
沒有不溢出的內存,看看下面的代碼:
運行的結果是“共申請了1916M內存,程序運行了 69421 個系統時鐘”,意思是在分配了1916M內存后,malloc已經不能夠申請到1M大小的內存塊了。
內存池在底層也是調用了malloc函數,因此內存池也是必然會溢出的。而且內存池可能會比直接調用malloc更早的溢出,看看下面的代碼:
運行的結果是“共申請了992M內存,程序運行了 1265 個系統時鐘”,意思是在分配了992M內存后,內存池已經不能夠申請到1M大小的內存塊了。
5.2 內存池的基本原理
從上面的兩個測試可以看出內存池要比malloc溢出早,我的機器內存是1.5G,malloc分配了1916M才溢出(顯然分配了虛擬內存),而內存池只分配了992M就溢出了。第二點是內存池溢出快,只用了1265微秒就溢出了,而malloc用了69421微秒才溢出。
這些差別是內存池的處理機制造成的,內存池對于內存分配的算法如下,以pool內存池為例:
1. pool初始化時帶有一個塊大小的參數memSize,那么pool剛開始會申請一大塊內存,例如其大小為32*memSize。當然它還會申請一些空間用以管理鏈表,為方便述說,這里忽略這些內存。
2. 用戶不停的申請大小為memSize的內存,終于超過了內存池的大小,于是內存池啟動重分配機制;
3. 重分配機制會再申請一塊大小為原內存池大小兩倍的內存(那么第一次會申請64*memSize),然后將這塊內存加到內存池的管理鏈表末尾;
4. 用戶繼續申請內存,終于又一次超過了內存池的大小,于是又一次啟動重分配機制,直至重分配時無法申請到新的內存塊。
5. 由于每次都是兩倍于原內存,因此當內存池大小達到了992M時,再一次申請就需要1984M,但是malloc最多只能申請到1916M,因此malloc失敗,內存池溢出。
通過以上原理也可以理解為什么內存池溢出比malloc溢出要快得多,因為它是以2的指數級來擴大內存池,真正調用malloc的次數約等于log2(1916),而malloc是實實在在進行了1916次調用。所以內存池只用了1秒多就溢出了,而malloc用了69秒。
5.3 內存池溢出的解決方法
對于malloc造成的內存溢出,一般來說沒有太多辦法可想?;旧暇褪菆笠粋€異?;蛘咤e誤,然后讓用戶關閉程序。當然有的程序會有內存自我管理功能,可以讓用戶選擇關閉一切次要功能來維持主要功能的繼續運行。
而對于內存池的溢出,還是可以想一些辦法的,因為畢竟系統內存還有潛力可挖。
第一個方法是盡量延緩內存池的溢出,做法是在程序啟動時就盡量申請最大的內存池,如果在程序運行很久后再申請,可能OS因為內存碎片增多而不能提供最大的內存池。其方法是在程序啟動時就不停的申請內存直到內存池溢出,然后清空內存池并開始正常工作。由于內存池并不會自動減小,所以這樣可以一直維持內存池保持最大狀態。
第二個方法是在內存池溢出時使用第二個內存池,由于第二個內存池可以繼續申請較小塊的內存,所以程序可繼續運行。代碼如下:
運行結果如下:

結果表明在第一個內存池溢出后,第二個內存池又提供了480M的內存。
5.4 內存池溢出的終極方案
如果無論如何都不能再申請到新的內存了,那么還是老老實實告訴用戶重啟程序吧。