c++內存分配優先使用內存池,而不是new,delete
轉載
原文出處:http://www.devdiv.net/home/space.php?uid=125&do=blog&id=364
認識一下new和delete的開銷:
new和delete首先會轉調用到malloc和free,這個大家應該很熟識了。很多人認為malloc是一個很簡單的操作,其實巨復雜,它會執行一個系統調用,從用戶態轉到內核態,該系統調用會鎖住內存硬件,然后通過鏈表的方式查找空閑內存,如果找到大小合適的,就把用戶的進程地址映射到內存硬件地址中,然后釋放鎖,返回用戶態。delete是一個反過程。
相對的,如果不是使用堆分配,而是直接在棧上分配,比如類型int,那么開銷就是把sp這個寄存器加上sizeof(int)。
內存池模式:
內存池就是預先分配好,放到進程空間的內存塊,用戶申請與釋放內存其實都是在進程內進行,SGI-STL的alloc遇到小對象時就是基于內存池的。只有當內存池空間不夠時,才會再從系統找一塊很大的內存。
內存池模式是如此之重要,以至于讓我想不明白為什么四人幫那本《設計模式》沒有把內存池列為基本模式,目前其它的教材,包括學院教材,實踐教材都沒有列出這個模式(講線程池模式的教材倒非常多)。可能他們認為這不屬于設計,而屬于具體實現吧。但我覺得這樣的后果是間接把很多c++ fans帶向低效的編碼方式。
sun公司就挺喜歡搞一些算法,用c++實現與java實現一遍,結果顯示c++的效率有時甚至比java低,很多c++高手看了之后都會覺得很難解,其實有玄機:java的new其實是基于內存池的,而c++的new是直接系統調用。
c++內存池模式的發展:
c++98標準之前,基本上大多數程序員沒用使用內存池,c++98 標準之后,內存池的使用也只是停留在STL內部的使用上,并沒有得到推廣。
其實我認為,STL的內存分配模式是一場變革,它不但包含內存分配的革命,也包含了內存管理(這個話題先放一邊)的革命,只是這場變革被很多人忽略了。也有一些人認為STL的內存分配方案有潛在問題,就是只管從系統分配,但卻永遠不會調用系統級的釋放,如果使用不當,程序拿住的內存會越來越多。我自己工作過的項目沒遇上過這樣的問題,但之前營帳報表組的一個容災項目倒是遇上了。不過STL的內存模式沒有推廣最大的原因還是因為alloc不是標準組件,以至于被人忽略了。
STL之后,一些c++ fans們開始搞出了幾套內部使用的內存池。為了項目需要,我自己也曾經做過一個。但這些都沒有很正式的公開,而且也不完美。
大概在200x年(-_-!),主導c++標準的一群牛人發起了一個叫boost的項目,才正式的把內存池帶到實用與標準化階段。
插入一點題外話:關于boost,很多人(包括我自己也曾經)產生誤解,認為它是準標準庫,是下一代標準庫。其實boost是套基礎建設,用來證明哪些方案是可行,哪些是不可行的,它里面的一些組件有可能會出局,也有可能不是以庫的方式存在,而是以語言核心的方式存在,下一代標準庫名字叫TR1,再一下代叫TR2(我對使用TR這個名字很費解,為什么不統一叫STL呢)。
new,delete調用與內存池調用的效率對比:
講了這么多費話,要到關鍵時候了,用事例來證明為什么要優先使用內存池。下面這段代碼是我很久以前的一段測試案例,細節上可能有點懂難,但流程還是清晰的:
#include <time.h>
#include <boost/pool/object_pool.hpp>
struct CCC
{
CCC() {}
char data[10];
};
struct SSS
{
SSS() {}
short data[10];
};
struct DDD
{
DDD() {}
double data[10];
};
// 把new,delete封裝為一個與boost::object_pool一樣的接口,以便于測試
template <typename element_type, typename user_allocator = boost::default_user_allocator_malloc_free>
class new_delete_alloc
{
public:
element_type* construct() { return new element_type; }
void destroy(element_type* const chunk) { delete chunk; }
};
template
<
template<typename, typename>
class allocator
>
double test_allocator()
{
// 使用了一些不規則的分配與釋放,增加內存管理的負擔
// 但總體流程還是很規則的,基本上不產生內存碎片,要不然反差效果會更大。
allocator<CCC> c_allc;
allocator<SSS> s_allc;
allocator<DDD> d_allc;
double re = 0; // 隨便作一些運算,仿止編譯器優化掉內存分配的代碼
for (unsigned int i = 0; i < 10000; ++i)
{
for (unsigned int j = 0; j < 10000; ++j)
{
CCC* pc = c_allc.construct();
SSS* ps = s_allc.construct();
re += pc->data[2];
c_allc.destroy(pc);
DDD* pd = d_allc.construct();
re += ps->data[2];
re += pd->data[2];
s_allc.destroy(ps);
d_allc.destroy(pd);
}
}
return re;
}
int main(int argc, char* argv[])
{
double re1 = 0;
double re2 = 0;
// 運行內存池測試時,基本上對我機器其它進程沒什么影響
time_t begin = time(0);
re1 = test_allocator<boost::object_pool>(); // 使用內存池boost::object_pool
time_t seporator = time(0);
// 運行到系統調用測試時,感覺機器明顯變慢,
// 如果再加上內存碎片的考慮,對其它進程的影響會更大。
std::cout << long(seporator - begin) << std::endl;
re2 = test_allocator<new_delete_alloc>(); // 直接系統調用
std::cout << long(time(0) - seporator) << std::endl;
std::cout << re1 << re2 << std::endl;
}
總結:
在一個100000000次的循環中,使用內存池是3秒,使用系統調用是93秒。
可能會有人覺得100000000這個數很大,93秒沒什么,但想一下,一個表有幾千萬行是很正常的,如果每行有十多列,每列有數據類型,數據長度,數據內容。如果在這樣的一個循環錯誤的使用了new和delete。
而且以上測試還沒有考慮到碎片的影響,以及運行該程序時對其它程序的影響。而且還有一點,就是機器的內存硬件容量越大,內存分配時,需要搜索的時間就可能越長,如果內存是多條共同工作的,影響就再進一步。
什么算是錯誤的使用呢,比如返回一個std::string給用戶,有人覺得new出來返回指針給用戶會更好,你可能會想到如果new的話,只產生一次string的構造,如果直接返回對象可能需要多次構造,所以new效率更高。但事實不是這樣,雖然在構造里會有字符串的分配,但其實這個分配是在內存池中進行的,而你直接的那個new就肯定是系統調用。
當然,有些情況是不可說用什么就用什么的,但如果可選的話,優先使用棧上的對象,其次考慮內存池,然后再考慮系統調用。
posted on 2009-08-26 10:46
李陽 閱讀(1156)
評論(0) 編輯 收藏 引用 所屬分類:
C++