重寫boost內存池

  最近在寫遊戲服務器網絡模塊的時候,須要用到內存池。大量玩家經過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__ */
View Code

  這樣,一個簡單的內存池就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的性能爲什麼如此不濟,實在令我不解。

相關文章
相關標籤/搜索