內存池除了減少內存申請和釋放的開銷之外還有什麼提升性能或者方便之處?

比如現有一段代碼申請內存頻繁,且內存空間申請大小不一,這樣如果用內存池事先分配好內存,會使內存分配更高效。有人說庫申請內存還要經過多線程加鎖,難道自己寫內存池不需要加鎖嗎(這個理由有點牽強?)?


自己寫內存池這事我十多年前還真干過,拿C++開發一款pdf閱讀軟體,發現處理某些包含很多小對象的pdf文件時有時一頁要一兩分鐘才能顯示,profile之後發現問題存在對象頻繁分配釋放上,於是寫了一個內存池來優化,然後做到秒開。幹了這麼幾件事情:

  1. 初始化時向系統申請大塊內存作內存池。
  2. 自己處理freelist,實現對不同尺寸大小對象用不同策略。
  3. 在對象前後各留出幾個位元組的額外欄位保存長度和其他信息,兼做越界檢測。

  4. 用宏把對象分配時的代碼文件,行和函數信息記錄到對象頭之前的額外區域以方便調試。
  5. 重載頻繁分配與釋放對象的new,new[], delete, delete[]來優化分配策略。
  6. 線程同步以及為消除同步開銷而為重要線程單獨開內存池。

    重要的事情說三遍:

    在profile確定瓶頸是內存分配且不可繞過時再考慮寫內存池。

    在profile確定瓶頸是內存分配且不可繞過時再考慮寫內存池。

    在profile確定瓶頸是內存分配且不可繞過時再考慮寫內存池


Windows里你可以用HeapCreate創建一個獨立的heap並設置合適的策略,比如不加鎖。大部分時候比你自己造的輪子要好。


http://accu.org/content/conf2008/Alexandrescu-memory-allocation.screen.pdf

http://www.cs.umass.edu/~emery/pubs/berger-oopsla2002.pdf

http://www.cs.umass.edu/~emery/talks/OOPSLA-2002.ppt

CiteSeerX — The Memory Fragmentation Problem: Solved?

動不動就 32GB 以上內存的伺服器真需要關心內存碎片問題嗎?

一句話:不要低估了各路 malloc 作者的實力,這些都是大神級的人物:Doug Lea、Ulrich Drepper、Poul-Henning Kamp、Jason Evans、Sanjay Ghemawat。

你說的「減少開銷/更高效」有數據支持嗎?


現代內存分配器一般是buddy及其變種,是有合併內存碎片的能力的。當然你說只分配不釋放,或者是有特殊規律的內存分配釋放過程,那當然還是專有的快一些。

為什麼標題是線程池?


性能倒也罷了,現代操作系統自帶的內存分配器的性能,不是你自己能夠輕易達到的。其實主要是為了特別的需求。

首先我們不難想到,在自己做的內存池裡,你可以監控到一切的內存分配事件。所以首先可以玩各種debug、profiling的花樣。

然後,對於特別的操作,還是有可能優化性能的。比如某種對象,在數據處理的時候被頻繁訪問,它的數量又特別大,尺寸又不太大。那麼我們可以單獨開一個pool,把所有的這種對象盡量連續地放在一起,可以有利於緩存命中。


如果是thread-local的內存池,是可以不用加鎖的。

不同生命周期的內存塊,分別在相應的內存池裡分配,方便釋放,也方便調試。

到時候整塊內存都不要了,而且不需要調用析構函數的話,可以一下子整個釋放。

遊戲里,從場景退到主菜單,就可以這麼做。


做網路設備,伺服器端開發和做應用開發的對於這個問題可能感受完全不一樣。

要處理的數據對象不一樣,網路和伺服器開發,大部分的數據並不駐留內存。

網路類的數據屬於stream類型的數據,也就是說這個數據對於CPU來說,在很長的時間之內只用一次。

但是應用類的數據,可能要頻繁駐留內存,例如遊戲,文件處理的客戶端,而且會對原數據頻繁增添數據。

當然有人會說HTML的靜態頁面難道不會駐留內存嗎?我了我說的只是通常情況。

對於第一種類型的數據,重新構建一個內存池沒有意義。在路由器上動態申請內存是一個非常愚蠢的行為,靜態預先分配往往有更好的效果。

但是對於做應用的人,總會覺得系統原有的東西不夠靈活,無法滿足要做得更好的要求。

「發明」一個新的內存池可能並不是想代替系統自帶的。

比如說QQ,我相信它肯定是自己發明了一套東西去管理數據。

如果它一啟動就靜態申請個256M內存,整天被程序員冷嘲熱諷的,怎麼玩?


對於現代操作系統對於內存池的依賴正在減弱,這裡有APR( Apache Portable Runtime)關於內存池的一段描述:

Most of libapr APIs are dependent on memory pool. By memory pool, you can easily manage a set of memory chunks. Imagine the case without memory pool system, where you allocate several memory chunks. You have to free each of them. If you have ten memory chunks, you have to free ten times, otherwise you would suffer from memory leak bugs. Memory pool solves this issue. After you allocate one memory pool, you can allocate multiple memory chunks from the pool. To free them, all you have to do is to destroy the memory pool. By which, you can free all the memory chunks. There are two good points. First, as stated above, it is defensive against memory leak bugs. Second, allocation costs of memory chunks become relatively lower. In a sense, memory pool forces you to obey a session-oriented programming. A memory pool is a kind of a session context, that is, a set of objects that have the same lifetimes. You can control a set of objects within a session context. At the beginning of a session you create a memory pool. Then, you create objects in the memory pool during the session. Note that you don"t need to care about their lifetimes. Finally, at the end of the session all you have to do is to destroy the memory pool.

REMARK: In general, objects lifetime control is the most difficult part in programming. Thus, there are many other techniques for it, such as smart pointer, GC(garbage collection) and so on. Note that it is a bit hard to use such techniques at the same time. Since memory pool is one of such techniques, you have to be careful about the mixture.

REMARK: In the future, memory pool would become less important than now in libapr. Please refer to http://mail-archives.apache.org/mod_mbox/apr-dev/200502.mbox/%3c1f1d9820502241330123f955f@mail.gmail.com%3e.


內存池在嵌入式開發也有應用。

之前做過的多功能一體機就是在開發之初就定下各個子系統可以使用的內存配額,並在啟動時統一進行分配和初始化。之後各個子系統再自行分配。

優點不在於性能,而是在於可以大大提高系統的穩定性和魯棒性。即使出現內存泄漏,也只會影響個別子系統而已,不會造成整機的崩潰。


分配計數,可以用來定位內存泄露


我來說個其他問題,在windows下,當32位的程序佔用內存過大(長期使用的內存在1.2G左右時),且分配頻繁,內存碎片化會相當嚴重,最終結果就是你雖然還有可能內存,但是已經無法分配出連續的512K以上的內存了,這個問題在windows xp和window 7下都存在,win7情況相對好些。

這個時候需要內存池,自己進行管理,可以避免內存碎片化。


如果你恰好只用一個線程幹事情,那你是可以寫一個沒有鎖的。如果你的內存是一邊算一邊申請,算完了整個幹掉的話,那做出來的專用內存池肯定比malloc/free要高效許多。當然你也不能用他來代替malloc/free了。


拋開內存申請和釋放的性能不談,

在實際工程開發過程中,還有一些調試性方面的需求。這也是重複造輪子的一大驅動力


很多現代分配器如Je或者Tc分配策咯和演算法比多數人自己造的輪子要複雜的多得多.

即便是古老的Dlmalloc在單線程下的分配演算法也是相當複雜的.

自己獨立寫內存池, 然後用freelist管理的好處無非就是定製化了一下, 這樣就跳過了底層分配器的某些演算法分支(尤其是最複雜的small chunk的部分). 自然速度會提升. 當然你自己寫的內存池除了自己的應用可以用, 對任何其他程序來說一般都沒有意義.

這實際上這就是定製和通用的矛盾. 就像某些定製的專用圖像處理IC速度比通用處理器快得多, 但你並不能說這東西更複雜更高明, 僅僅是定製化而已.


雲風 貌似不上知乎,那我來引個雲風昨天寫的allocator吧:

https://github.com/cloudwu/lalloc

再援引一句雲風在群里說的原話,我不知道是不是斷章取義了,僅供參考哈,「不了解分配器怎麼工作的才會去想再寫個內存池吧」


建議閱讀GNU開源的內存池的代碼,可以用來移植使用。內存池最主要避免的是內存碎片,通常來說可能會有字內存池,和塊內存池,良好的內存使用習慣會使代碼更加穩定


a. 調試內存泄露問題方便一點

b. 面向特定應用的演算法,也許比通用的演算法有優勢

其實沒必要 :)


推薦閱讀:

為什麼c++不能把「= [] () ->」操作符重載為非成員函數?
為什麼這些年c語言統治了幾乎所有方面?
怎麼理解 `auto var = [&]() { /* things to do */ }`?
為什麼 C++ 列表初始化時會執行兩次拷貝構造函數?
寫循環語句,循環體部分理論上是不是可以都寫在for(;;)第二個分號後?

TAG:資料庫 | 操作系統 | C編程語言 | 內存管理 | C |