最近在写游戏服务器网络模块的时候,需要用到内存池。大量玩家通过tcp连接到服务器,通过大量的消息包与服务器进行交互。因此要给每个tcp分配收发两块缓冲区。那么这缓冲区多大呢?通常游戏操作的消息包都很小,大概几十字节。但是在玩家登录时或者卡牌游戏发战报(将整场战斗打完,生成一个消息包),包的大小可能达到30k或者更大,取决于游戏设定。这些缓冲区不可能使用glibc原始的new、delete来分配,这样可能会造成严重的内存碎片,并且效率也不高。

  于是我们要使用内存池。并且是等长内存池,即每次分配的内存只能是基数的整数倍。如把内存池基数设置为1024,则每次分配的内存只能是1024,2048,3072,4096...这样利用率高,易管理。boost提供这种内存池,下面我们来看下boost如何实现内存池。(下面的解说需要你先了解一下boost池的源码才能看明白)

  boost的池在boost/pool/pool.hpp中实现,我们先把它简化一下:

class PODptr
{
char * ptr;
unsigned int sz;
} struct pool:public simple_segregated_storage
{
PODptr<size_type> list;
}

  pool只包括一个PODptr的成员list,它其实是一个巧妙的链表。PODptr则是指向一块用new分配出来的原始内存。

  假如我们要分配一块等长内存,则要调用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分成下图所示:

node的ptr指向这块内存的起始地址,sz则表示这块内存的大小。在这块内存的末尾,boost预留了一个指针及一个int的位置,这个指针指向下一块block的起始地址,int则存着下一块block的大小。每从系统获取一块block,boost就利用next_ptr把它链接表list里,形成一个链表。

  而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 > ? cur : ; \
while ( size < (uint32)cnt ) \
{ \
size *= ; \
} \
type *tmp = new type[size]; \
init( tmp,sizeof(type)*size ); \
if ( cur > ) \
memcpy( tmp,base,sizeof(type)*cur ); \
delete []base; \
base = tmp; \
cur = size; \
} #define array_zero(base,size) \
memset ((void *)(base), , size) template<uint32 ordered_size,uint32 chunk_size = >
class ordered_pool
{
public:
ordered_pool();
~ordered_pool(); char *ordered_malloc( uint32 n = );
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 = ;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(),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 = ; 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 > ) );
array_resize( NODE,anpts,anptmax,n+,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 - ,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( );
} int main()
{
std::set_new_handler( memory_fail ); const int max = ; ordered_pool<CHUNK_SIZE> pool;
char *(list[][max]) = {}; clock_t start = clock();
for ( int i = ;i < max;i ++ )
{
list[][i] = pool.ordered_malloc( );
list[][i] = pool.ordered_malloc( );
//list[3][i] = pool.ordered_malloc( 4 );
}
for ( int i = ;i < max;i ++ )
{
pool.ordered_free( list[][i], );
pool.ordered_free( list[][i], );
//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 ;
}

  因为虚拟机只有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 = ; int main ( )
{
boost::pool<> p(sizeof(int)); int* vec1[MAXLENGTH];
int* vec2[MAXLENGTH]; clock_t clock_begin = clock(); for (int i = ; i < MAXLENGTH; ++i)
vec1[i] = static_cast<int*>(p.malloc()); for (int i = ; i < MAXLENGTH; ++i)
p.free(vec1[i]); clock_t clock_end = clock(); cout << "程序运行了 " << clock_end-clock_begin << " 个系统时钟" << endl; clock_begin = clock(); for (int i = ; i < MAXLENGTH; ++i)
vec2[i] = new int(); for (int i = ; i < MAXLENGTH; ++i)
delete vec2[i]; clock_end = clock(); cout << "程序运行了 " << clock_end-clock_begin << " 个系统时钟" << endl; return ;
}

原作者的测试环境为测试环境:VS2008,WindowXP SP2,Pentium 4 CPU双核,1.5GB内存,连续申请和连续释放10万块内存。测试结果:

我把这份代码放到我的虚拟机上测试:

g++ -o test test.cpp -lboost_system
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟
xzc@xzc-VirtualBox:~/code/pool$ ./test
程序运行了 个系统时钟
程序运行了 个系统时钟

显然boost比glibc还是差得远,可能是glibc和windows的内存分配算法问题。

  到此,内存池的测试告一段落。只是boost的性能为何如此不济,实在令我不解。

重写boost内存池的更多相关文章

  1. Boost内存池使用与测试

    目录 Boost内存池使用与测试 什么是内存池 内存池的应用场景 安装 内存池的特征 无内存泄露 申请的内存数组没有被填充 任何数组内存块的位置都和使用operator new[]分配的内存块位置一致 ...

  2. 基于C/S架构的3D对战网络游戏C++框架 _05搭建系统开发环境与Boost智能指针、内存池初步了解

    本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): ...

  3. 定长内存池之BOOST::pool

    内存池可有效降低动态申请内存的次数,减少与内核态的交互,提升系统性能,减少内存碎片,增加内存空间使用率,避免内存泄漏的可能性,这么多的优点,没有理由不在系统中使用该技术. 内存池分类: 1.      ...

  4. boost的线程池和内存池 智能指针

    内存池为boost自带的 #include <boost/pool/pool.hpp> 或者另外一个开源的库: nedmalloc 一个高效率的库 线程池需要下载另外一个开源库 http: ...

  5. boost之内存池

    讲到内存池我们会想到对对象进行动态分配的过程new包含三个过程 1.使用operator new分配内存 2.使用placement new 初始化 3.返回内存地址. 分配内存可以分解成分配内存和获 ...

  6. nginx——内存池篇

    nginx--内存池篇 一.内存池概述 内存池是在真正使用内存之前,预先申请分配一定数量的.大小相等(一般情况下)的内存块留作备用.当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续 ...

  7. 不定长内存池之apr_pool

    内存池可有效降低动态申请内存的次数,减少与内核态的交互,提升系统性能,减少内存碎片,增加内存空间使用率,避免内存泄漏的可能性,这么多的优点,没有理由不在系统中使用该技术. 内存池分类: 1.      ...

  8. Linux编程之内存池的设计与实现(C++98)

    假设服务器的硬件资源"充裕",那么提高服务器性能的一个很直接的方法就是空间换时间,即"浪费"服务器的硬件资源,以换取其运行效率.提升服务器性能的一个重要方法就是 ...

  9. 初识nginx——内存池篇

    初识nginx——内存池篇 为了自身使用的方便,Nginx封装了很多有用的数据结构,比如ngx_str_t ,ngx_array_t, ngx_pool_t 等等,对于内存池,nginx设计的十分精炼 ...

随机推荐

  1. 大数据笔记01:大数据之Hadoop简介

    1. 背景 随着大数据时代来临,人们发现数据越来越多.但是如何对大数据进行存储与分析呢?   单机PC存储和分析数据存在很多瓶颈,包括存储容量.读写速率.计算效率等等,这些单机PC无法满足要求. 2. ...

  2. Linux shell入门基础(二)

    二.shell对文本的操作 01.查看文本的命令 #cat /etc/passwd(并非对文本文件操作) #tail -5 /etc/passwd(查看末尾5行) #tail -f /var/log/ ...

  3. java的练习

    import java.awt.GridLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.s ...

  4. 关于数据库中varchar/nvarchar类型数据的获取注意事项

    当在页面后台获取数据库表中某字段的数据时,需注意该数据的类型.防止因实际数据的字符长度因达不到指定数据类型规定的字符长度而导致空格的占位符. 比如: MSSQL中某一表的结构如下:   表中的数据: ...

  5. 电厂MIS,SIS简介

    MIS(Management Information System)管理信息系统,主要指的是进行日常事务操作的系统,它使管理人员及时了解公司现状和各种消息,它是电力企业管理现代化的重要标志. 一个典型 ...

  6. Hbase常用操作

    下面我们看看HBase Shell的一些基本操作命令,我列出了几个常用的HBase Shell命令,如 名称 命令表达式 创建表 create '表名称', '列名称1','列名称2','列名称N' ...

  7. Android自定义扁平化对话框

    平时我们开发的大多数的Android.iOS的APP,它们的风格都是拟物化设计.如今Android 4.X.iOS 7.WP8采用的是扁平化设计,可以看出扁平化设计是未来UI设计的趋势.其实扁平化设计 ...

  8. poj1204之AC自动机

    Word Puzzles Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 8235   Accepted: 3104   Sp ...

  9. 初探react

    知道这个框架有一段时间了,可是项目中没有用到,也懒得整理,最近两天比较清闲,又想起了它.好了,废话不多说了,都是干货. react是个什么框架? 为什么用react? react 的原理 react有 ...

  10. IE的有条件注释详解

    IE的有条件注释是一种专有的(因此是非标准的).对常规(X)HTML注释的Miscrosoft扩展.顾名思义,有条件注释使你能够根据条件(比如浏览器版本)显示代码块.尽管是非标准的,但是有条件注释对于 ...