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

結(jié)論:在連續(xù)申請(qǐng)和連續(xù)釋放10萬塊內(nèi)存的情況下,使用內(nèi)存池耗時(shí)是使用new耗時(shí)的47.46%。
3.2 測(cè)試2:反復(fù)申請(qǐng)和釋放小塊內(nèi)存
代碼如下:
測(cè)試結(jié)果如下:

結(jié)論:在反復(fù)申請(qǐng)和釋放50萬次內(nèi)存的情況下,使用內(nèi)存池耗時(shí)是使用new耗時(shí)的64.34%。
3.3 測(cè)試3:反復(fù)申請(qǐng)和釋放C++對(duì)象
C++對(duì)象在動(dòng)態(tài)申請(qǐng)和釋放時(shí),不僅要進(jìn)行內(nèi)存操作,同時(shí)還要調(diào)用構(gòu)造和析購函數(shù)。因此有必要對(duì)C++對(duì)象也進(jìn)行內(nèi)存池的測(cè)試。
代碼如下:
測(cè)試結(jié)果如下:

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

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