最近在寫遊戲服務器網絡模塊的時候,須要用到內存池。大量玩家經過tcp鏈接到服務器,經過大量的消息包與服務器進行交互。所以要給每一個tcp分配收發兩塊緩衝區。那麼這緩衝區多大呢?一般遊戲操做的消息包都很小,大概幾十字節。可是在玩家登陸時或者卡牌遊戲發戰報(將整場戰鬥打完,生成一個消息包),包的大小可能達到30k或者更大,取決於遊戲設定。這些緩衝區不可能使用glibc原始的new、delete來分配,這樣可能會形成嚴重的內存碎片,而且效率也不高。html
因而咱們要使用內存池。而且是等長內存池,即每次分配的內存只能是基數的整數倍。如把內存池基數設置爲1024,則每次分配的內存只能是1024,2048,3072,4096...這樣利用率高,易管理。boost提供這種內存池,下面咱們來看下boost如何實現內存池。(下面的解說須要你先了解一下boost池的源碼才能看明白)node
boost的池在boost/pool/pool.hpp中實現,咱們先把它簡化一下:linux
class PODptr { char * ptr; unsigned int sz; } struct pool:public simple_segregated_storage { PODptr<size_type> list; }
pool只包括一個PODptr的成員list,它實際上是一個巧妙的鏈表。PODptr則是指向一塊用new分配出來的原始內存。ios
假如咱們要分配一塊等長內存,則要調用ordered_malloc。咱們先假設是第一次調用,還不存在緩存。算法
void *pool:ordered_malloc(n) { char *block = malloc() simple_segregated_storage.add_ordered_block() const PODptr node(); list.add_node(node); return ptr; }
boost先調用malloc來分配一塊大內存block,建立了一個PODptr對象node來管理這塊內存。這塊內存被boost分紅下圖所示:ubuntu
node的ptr指向這塊內存的起始地址,sz則表示這塊內存的大小。在這塊內存的末尾,boost預留了一個指針及一個int的位置,這個指針指向下一塊block的起始地址,int則存着下一塊block的大小。每從系統獲取一塊block,boost就利用next_ptr把它連接表list裏,造成一個鏈表。windows
而block裏前面這一塊空白的內存blank,則經過add_ordered_block交給pool的基類simple_segregated_storage來管理。數組
struct simple_segregated_storage { void *first }
simple_segregated_storage是管理空白可用的內存塊的,它把空白的內存分紅等長的小塊,而後用鏈表將它們鏈接起來,first就是表頭。那麼,只有一個void指針如何實現一個鏈表呢?由於simple_segregated_storage管理的內存都是空白的,因此你能夠隨意往裏面寫東西。因而simple_segregated_storage把下一塊內存寫在空白的內存塊上:緩存
釋放內存時調用ordered_free,原理就是把釋放的內存還給simple_segregated_storage,放到鏈表就好。服務器
因而,boost內存池的原理能夠總結爲pool先從系統獲取一大塊內存,simple_segregated_storage把它分紅幾小塊放到鏈表上,用到時就從鏈表上取。用沒了又重系統取...
上面說了如何取一塊內存,若是我要分配4塊大小的內存,boost又是如何處理呢?simple_segregated_storage將一大塊內存分紅小塊時,這些小塊內存的地址是連續的。boost會遍歷已有的鏈表,將這些小塊插入到合適的位置,保證整條鏈表節點的地址都是由小到大的。當你要分配多塊內存時,boost會遍歷鏈表,對比每一個節點和下一個節點的地址是否連續,若是找到合適的大小的連續內存塊,則將這幾塊分配出去。固然,釋放的時候也要遍歷鏈表,插入到對應的位置。
咱們能夠看到,boost等長內存池的實如今分配、釋放時都要遍歷鏈表,顯示不是最優的。因而我決定重寫一個。我重寫的理由很簡單:boost的庫是一個通用的庫,但對於個人場景,卻不是最優的。根據個人場景,我假定個人緩衝區大小基數爲8192,則我須要分配的內存爲8k,16k,32k,64k。再大就不處理了,遊戲邏輯中64k以上的通訊比較罕見,能夠當異常處理了。假如咱們有5w連接,緩衝區平均大小爲16k,則每一個連接爲32k,這個消耗對於如今的服務器還可能忍受。因而,我把內存池簡化一下:以n(1,2,3,4)爲下標生成一個數組,每一個數組元素是一塊鏈表,對應8k,16k,32k,64k。每次分配直接從鏈表取,釋放時放回對應的鏈表,無需遍歷。
#ifndef __ORDERED_POOL_H__ #define __ORDERED_POOL_H__ /* 等長內存池,參考了boost內存池(boolst/pool/pool.hpp).分配的內存只能是ordered_size * 的n倍。每個n都造成一個空閒鏈表,利用率比boost低。 * 1.分配出去的內存再也不受池的管理 * 2.全部內存在池銷燬時會釋放(包括未歸還的) * 3.沒有約束內存對齊。所以用的是系統默認對齊,在linux 32/64bit應該是OK的 * 4.最小內存塊不能小於一個指針長度(4/8 bytes) */ #include <cassert> #include <cstring> typedef int int32; typedef unsigned int uint32; #define array_resize(type,base,cur,cnt,init) \ if ( (cnt) > (cur) ) \ { \ uint32 size = cur > 0 ? cur : 16; \ while ( size < (uint32)cnt ) \ { \ size *= 2; \ } \ type *tmp = new type[size]; \ init( tmp,sizeof(type)*size ); \ if ( cur > 0) \ memcpy( tmp,base,sizeof(type)*cur ); \ delete []base; \ base = tmp; \ cur = size; \ } #define array_zero(base,size) \ memset ((void *)(base), 0, size) template<uint32 ordered_size,uint32 chunk_size = 512> class ordered_pool { public: ordered_pool(); ~ordered_pool(); char *ordered_malloc( uint32 n = 1 ); void ordered_free ( char * const ptr,uint32 n ); private: typedef void * NODE; NODE *anpts; /* 空閒內存塊鏈表數組,倍數n爲下標 */ uint32 anptmax; void *block_list; /* 從系統分配的內存塊鏈表 */ /* 一塊內存的指針是ptr,這塊內存的前幾個字節儲存了下一塊內存的指針地址 * 即ptr能夠看做是指針的指針 * nextof返回這地址的引用 */ inline void * & nextof( void * const ptr ) { return *(static_cast<void **>(ptr)); } /* 把從系統獲取的內存分紅小塊存到鏈表中 * 這些內存塊都是空的,故在首部建立一個指針,存放指向下一塊空閒內存的地址 */ inline void *segregate( void * const ptr,uint32 partition_sz, uint32 npartition,uint32 n ) { char *last = static_cast<char *>(ptr); for ( uint32 i = 1;i < npartition;i ++ ) { char *next = last + partition_sz; nextof( last ) = next; last = next; } nextof( last ) = anpts[n]; return anpts[n] = ptr; } }; template<uint32 ordered_size,uint32 chunk_size> ordered_pool<ordered_size,chunk_size>::ordered_pool() : anpts(NULL),anptmax(0),block_list(NULL) { assert( ("ordered size less then sizeof(void *)",ordered_size >= sizeof(void *)) ); } template<uint32 ordered_size,uint32 chunk_size> ordered_pool<ordered_size,chunk_size>::~ordered_pool() { if ( anpts ) delete []anpts; anpts = NULL; anptmax = 0; while ( block_list ) { char *_ptr = static_cast<char *>(block_list); block_list = nextof( block_list ); delete []_ptr; } } /* 分配N*ordered_size內存 */ template<uint32 ordered_size,uint32 chunk_size> char *ordered_pool<ordered_size,chunk_size>::ordered_malloc( uint32 n ) { assert( ("ordered_malloc n <= 0",n > 0) ); array_resize( NODE,anpts,anptmax,n+1,array_zero ); void *ptr = anpts[n]; if ( ptr ) { anpts[n] = nextof( ptr ); return static_cast<char *>(ptr); } /* 每次固定申請chunk_size塊大小爲(n*ordered_size)內存 * 不用指數增加方式由於內存分配過大可能會失敗 */ uint32 partition_sz = n*ordered_size; uint32 block_size = sizeof(void *) + chunk_size*partition_sz; char *block = new char[block_size]; /* 分配出來的內存,預留一個指針的位置在首部,用做鏈表將全部從系統獲取的 * 內存串起來 */ nextof( block ) = block_list; block_list = block; /* 第一塊直接分配出去,其餘的分紅小塊存到anpts對應的連接中 */ segregate( block + sizeof(void *) + partition_sz,partition_sz, chunk_size - 1,n ); return block + sizeof(void *); } template<uint32 ordered_size,uint32 chunk_size> void ordered_pool<ordered_size,chunk_size>::ordered_free( char * const ptr,uint32 n ) { assert( ("illegal ordered free",anptmax >= n && ptr) ); nextof( ptr ) = anpts[n]; anpts[n] = ptr; } #endif /* __ORDERED_POOL_H__ */
這樣,一個簡單的內存池就OK了,利用率下降了,但速度上去了。下面咱們來與boost(1.59)對比一下:
#include <iostream> #include <cstdio> #include <ctime> #include <cstdlib> #include "ordered_pool.h" //#include <boost/pool/singleton_pool.hpp> #define CHUNK_SIZE 8192 void memory_fail() { std::cerr << "no memory anymore !!!" << std::endl; exit( 1 ); } int main() { std::set_new_handler( memory_fail ); const int max = 10000; ordered_pool<CHUNK_SIZE> pool; char *(list[4][max]) = {0}; clock_t start = clock(); for ( int i = 0;i < max;i ++ ) { list[0][i] = pool.ordered_malloc( 1 ); list[1][i] = pool.ordered_malloc( 2 ); //list[3][i] = pool.ordered_malloc( 4 ); } for ( int i = 0;i < max;i ++ ) { pool.ordered_free( list[0][i],1 ); pool.ordered_free( list[1][i],2 ); //pool.ordered_free( list[3][i],4 ); } std::cout << "my pool run:" << float(clock() - start)/CLOCKS_PER_SEC << std::endl; /* //typedef boost::singleton_pool<char, CHUNK_SIZE> Alloc; boost::pool<> Alloc(CHUNK_SIZE); clock_t _start = clock(); for ( int i = 0;i < 10000;i ++ ) { list[0][i] = (char*)Alloc.ordered_malloc( 1 ); list[1][i] = (char*)Alloc.ordered_malloc( 2 ); //list[3][i] = (char*)Alloc::ordered_malloc( 4 ); } for ( int i = 0;i < 10000;i ++ ) { Alloc.ordered_free( list[0][i],1 ); Alloc.ordered_free( list[1][i],2 ); //Alloc::ordered_free( list[3][i],4 ); } std::cout << "boost run:" << float(clock() - _start)/CLOCKS_PER_SEC << std::endl; */ return 0; }
由於虛擬機只有2G內存,加上ubuntu佔用了部份內存,我經過註釋代碼來分別測試效果。
xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.035874 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.035968 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.027455 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.03688 boost run:8.42273 xzc@xzc-VirtualBox:~/code/pool$ ./main boost run:8.50574 xzc@xzc-VirtualBox:~/code/pool$ ./main boost run:8.48862 std run:0.004238 xzc@xzc-VirtualBox:~/code/pool$ ./main std run:0.003537 xzc@xzc-VirtualBox:~/code/pool$ ./main std run:0.00356 xzc@xzc-VirtualBox:~/code/pool$ ./main std run:0.003925
測試結果讓我大跌眼鏡。my pool run是我寫的庫運行時間,boost run表示boost庫的時間,std run則表示glibc new delete的運行時間。能夠看到glibc是最優的,其次是我寫的庫,而boost徹底不在一個級別上。考慮到glibc解決不了內存碎片的問題,並且內存池在第一次分配上會吃虧(得先調用一次new從系統獲取內存),因而在第一次釋放後,再測試一次,此次沒有測試glibc
my pool run:0.000949 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.001046 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.001133 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.001005 xzc@xzc-VirtualBox:~/code/pool$ ./main my pool run:0.001074 xzc@xzc-VirtualBox:~/code/pool$ ./main boost run:2.14777 xzc@xzc-VirtualBox:~/code/pool$ ./main boost run:2.15328 xzc@xzc-VirtualBox:~/code/pool$ ./main boost run:2.15201 xzc@xzc-VirtualBox:~/code/pool$ ./main boost run:2.15536
能夠看到此次內存池的速度加快了很多,我本身的庫已經超過glibc了,但boost的速度依然慘不忍睹。我不得不懷疑是否是我不會用boost。因而在網上(http://tech.it168.com/a2011/0726/1223/000001223399_all.shtml,在cnblogs、oschina、csdn上都沒找到更好的測試代碼)找了些代碼:
#include <iostream> #include <ctime> #include <boost/pool/pool.hpp> #include <boost/pool/object_pool.hpp> using namespace std; using namespace boost; const int MAXLENGTH = 100000; int main ( ) { boost::pool<> p(sizeof(int)); int* vec1[MAXLENGTH]; int* vec2[MAXLENGTH]; clock_t clock_begin = clock(); for (int i = 0; i < MAXLENGTH; ++i) vec1[i] = static_cast<int*>(p.malloc()); for (int i = 0; i < MAXLENGTH; ++i) p.free(vec1[i]); clock_t clock_end = clock(); cout << "程序運行了 " << clock_end-clock_begin << " 個系統時鐘" << endl; clock_begin = clock(); for (int i = 0; i < MAXLENGTH; ++i) vec2[i] = new int(); for (int i = 0; i < MAXLENGTH; ++i) delete vec2[i]; clock_end = clock(); cout << "程序運行了 " << clock_end-clock_begin << " 個系統時鐘" << endl; return 0; }
原做者的測試環境爲測試環境:VS2008,WindowXP SP2,Pentium 4 CPU雙核,1.5GB內存,連續申請和連續釋放10萬塊內存。測試結果:
我把這份代碼放到個人虛擬機上測試:
g++ -o test test.cpp -lboost_system xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 12781 個系統時鐘 程序運行了 7431 個系統時鐘 xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 14078 個系統時鐘 程序運行了 10028 個系統時鐘 xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 11624 個系統時鐘 程序運行了 7787 個系統時鐘 xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 13270 個系統時鐘 程序運行了 9534 個系統時鐘 xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 14641 個系統時鐘 程序運行了 12354 個系統時鐘 xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 14127 個系統時鐘 程序運行了 11137 個系統時鐘 xzc@xzc-VirtualBox:~/code/pool$ ./test 程序運行了 10371 個系統時鐘 程序運行了 6878 個系統時鐘
顯然boost比glibc仍是差得遠,多是glibc和windows的內存分配算法問題。
到此,內存池的測試告一段落。只是boost的性能爲什麼如此不濟,實在令我不解。