内存分配器

STL的Allocator

 简介
Allocator分配器是STL的重要组件,负责为容器Container中的元素分配和释放空间,前面也说过将Allocator独立出来的好处就是可以实现不同的分配策略,因而可以根据需要使分配尽可能的最优化;而且这也使定制自己的分配器成为可能。
Allocator分配的空间不一定就是内存,你可以从任何可行的地方分配内存,比如磁盘。

 Allocator的标准接口
为了给各种Container提供空间分配/释放的能力,Allocator必须具有一组标准的接口,直接照搬列出了,全部死记这些对能力也不会有什么提高,重要的是能够自己根据需要实现一个Allocator就行了。
2.1 第一组
各种type类型,你可能会很奇怪,为什么搞这么复杂呢,刚开始时我也有同样的疑惑;其实这个跟traits编程方法相关了,内容还不少,放到后面再说吧。
typedef  T         value_type;
typedef  T*        pointer;
typedef  const T*   const_pointer;
typedef  T&        reference
typedef  const T&   const_reference
typedef  size_t      size_type
typedef  ptrdiff_t    difference_type
2.2 第二组
构造和析构函数
Allocator::Allocator()缺省构造函数
Allocator::Allocator(const Allocator&) copy构造函数
template <class U> Allocator::Allocator(const Allocator<U> &) 泛化的copy构造函数
Allocator::~Allocator() 缺省析构函数
还有一个rebind函数,llocator::rebind,一个nested class templateclass rebind<U>有唯一的成员other,是一个typedef,代表Allcator<U>

2.3 第三组
这是一组空间的释放与回收函数
pointer Allocator::allocate(size_type n, const void* = 0)
配置空间,足以缓存n个对象,第二个参数是个提示,实际上可能会用来增进区域性(locality),或完全忽略之。
void Allocator::deallocate(pointer p, size_type n)归还先前分配的空间
size_type Allocator::max_size() const 返回可分配的最大空间

2.4 第四组
一组取地址函数
pointer Allocator::address(reference x) const
返回x的地址,等同于&x
const_pointer Allocator::address(const_reference x) const
返回x的地址,等同于&x
2.5 第五组
constructdestroy函数
void Allocator::construct(pointer p, cosnt T&x)等同于new((const void*)p) T(x)
void Allocator::destroy(pointer p) 等同于p->~T()

Allocator可以非常简单,简单的执行内存分配和释放即可,也可以非常复杂,像SGI的分配器;这个应该根据就事而论。

 SGI分配器
顺便提一下,SGI的分配器并没有遵照STL标准,为了减少内存碎片和分配效率,SGI的分配器实现是相当复杂的,它提供了两级分配器,对于大于128B的请求,采用第一级分配器,就是直接malloc,释放就是直接free;小于128B的分配器则采用了第二级分配器;看到这一部分时,让我想起了 Linux中的Slab分配器,专门针对小内存的分配策略,经常网上有人争论内存碎片问题,应该是根据操作系统而异的,像Linux应该是不存在这样的问题的,本身的小内存分配做的就是相当出色,这是题外话了。

先到这里吧,毕竟Allocator的内容还是相当多的。

usidc52011-11-01 12:33
今天把SGI提供的Allocator分配器仔细看了下,其设计还是相当精巧的。不过SGI的分配器已经脱离了STL标准,比如它就没有实现construct()destroy()成员函数。

 简单malloc分配器
对于大于128B的空间直接就是malloc()free()了,没有什么特殊的;不过它还是仿造C++new handler形式设置了一个malloc exception handler;这样就和C++new行为相一致了,你可以设置malloc失败时的exception handler

 Newdelete的分离
当对一个对象调用newdelete时,这两个操作都包含了两个阶段的动作:调用operator new分配空间,然后调用该对象的构造函数;调用该对象的析构函数,然后调用operator delete释放空间;SGI的分配器将这两步做了分离,内存分配和释放由函数alloc::allocate()alloc::deallocate()负责;物体构造和析构由函数construct()destroy()负责。

在调用destroy()函数同时释放n个对象(假设类型为T)时,SGI提供了方法可以判定对象是否有non-trivial destructor,如果没有则不必要循环为每个对象调用T::~T(),以提高效率,贴上源码,以便查看:
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
  new (p) T1(value);
}
template <class T>
inline void destroy(T* pointer) {
    pointer->~T();
}
template <class ForwardIterator>
inline void // 具有non trivial destructor
__destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
  for ( ; first < last; ++first)
    destroy(&*first);
}
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator, ForwardIterator, __true_type) {} //空函数体,trivial destructor不需要调用
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
  typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
  __destroy_aux(first, last, trivial_destructor());
}
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
  __destroy(first, last, value_type(first));
}
这需要借助于Traits编程技法来完成(原书3.7节)的:
typedef typename __type_traits<T>::has_trivial_destructor  trivial_destructor;
首先使用value_type()获取迭代器指向的物体类型,然后使用__type_traits<T>查看T是否有non-trivial destructor

 简单分配器 simple_alloc
SGI为这原始分配器malloc和次级分配器alloc所作的一层简单封装;

 次级分配器alloc
对于小于128B的请求采用了次级分配器;说白了就是SGI维护一个内存池来处理这些请求,以保证效率。它会将请求的字节数n圆整到8的倍数,比如如果请求的是14B的空间,其实获得的是16B;从这也可以得出SGI一共有16个链表需要维护,每个链表对应一个分配级别,对应的内存块大小分别是:81624128
为了不浪费存储空间,链表是一个union结构,像这样:
       union OBJ{
              union OBJ *free;
              char *data[1];
       };
free指针指向的是free链表中下一个空闲内存块,如果一个内存块被分配出去,就交给用户程序维护了,那么该块就没有必要继续维护了,知道再次收回(通过 free())。
分配和回收函数allocate()deallocate()就是简单的链表操作了,没有特别的地方。
比较复杂的就是内存池的维护函数chunk_alloc()函数,它最终还是需要通过malloc()来申请内存新的空间。

 辅助函数
最后是几个操作为初始化空间的辅助函数,其内部基本都是通过调用全局construct()函数完成的。

 对别人不是问题的问题
读到最后一直有个疑惑就是allocator没有实现construct()函数,那么它和容器是如何协同工作的呢?搜了搜源文件才发现,原来各容器都会显式调用全局函数construct()construct函数实际调用placement new),比如下面是list容器模板的一段代码:
typedef simple_alloc<list_node, Alloc> list_node_allocator;
link_type get_node() { return list_node_allocator::allocate(); }
void put_node(link_type p) { list_node_allocator::deallocate(p); }
  link_type create_node(const T& x) {
    link_type p = get_node();
    __STL_TRY {
      construct(&p->data, x);
    }
    __STL_UNWIND(put_node(p));
    return p;
  }
  void destroy_node(link_type p) {
    destroy(&p->data);
    put_node(p);
  }

usidc52011-11-01 12:34
题记:内存管理一直是C/C++程序的红灯区。关于内存管理的话题,大致有两类侧重点,一类是内存的正确使用,例如C++中new和delete应该成对出现,用RAII技巧管理内存资源,auto_ptr等方面,很多C/C++书籍中都使用技巧的介绍。另一类是内存管理的实现,如linux内核的slab分配器,STL中的allocator实现,以及一些特定于某种对象的内存管理等。最近阅读了一些内存管理实现方面的资料和源码,整理了一下,汇编成一个系列介绍一些常用的内存管理策略。
1. STL容器简介
STL提供了很多泛型容器,如vector,list和map。程序员在使用这些容器时只需关心何时往容器内塞对象,而不用关心如何管理内存,需要用多少内存,这些STL容器极大地方便了C++程序的编写。例如可以通过以下语句创建一个vector,它实际上是一个按需增长的动态数组,其每个元素的类型为int整型:
stl::vector<int> array;
拥有这样一个动态数组后,用户只需要调用push_back方法往里面添加对象,而不需要考虑需要多少内存:
array.push_back(10); 
array.push_back(2);

vector会根据需要自动增长内存,在array退出其作用域时也会自动销毁占有的内存,这些对于用户来说是透明的,stl容器巧妙的避开了繁琐且易出错的内存管理工作。
2. STL的默认内存分配器
隐藏在这些容器后的内存管理工作是通过STL提供的一个默认的allocator实现的。当然,用户也可以定制自己的allocator,只要实现allocator模板所定义的接口方法即可,然后通过将自定义的allocator作为模板参数传递给STL容器,创建一个使用自定义allocator的STL容器对象,如:
stl::vector<int, UserDefinedAllocator> array;
大多数情况下,STL默认的allocator就已经足够了。这个allocator是一个由两级分配器构成的内存管理器,当申请的内存大小大于128byte时,就启动第一级分配器通过malloc直接向系统的堆空间分配,如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。
这种做法有两个优点:
1)小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,整个过程类似于批发和零售,起先是由allocator向总经商批发一定量的货物,然后零售给用户,与每次都总经商要一个货物再零售给用户的过程相比,显然是快捷了。当然,这里的一个问题时,内存池会带来一些内存的浪费,比如当只需分配一个小对象时,为了这个小对象可能要申请一大块的内存池,但这个浪费还是值得的,况且这种情况在实际应用中也并不多见。
2)避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。以内存池组织小对象的内存,从系统的角度看,只是一大块内存池,看不到小对象内存的分配和释放。
实现时,allocator需要维护一个存储16个空闲块列表表头的数组free_list,数组元素i是一个指向块大小为8*(i+1)字节的空闲块列表的表头,一个指向内存池起始地址的指针start_free和一个指向结束地址的指针end_free。空闲块列表节点的结构如下:
union obj { 
        union obj *free_list_link; 
        char client_data[1]; 
};

这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的时用户的数据。因此,allocator中的空闲块链表可以表示成:
obj* free_list[16];
3. 分配算法
allocator分配内存的算法如下:
算法:allocate
输入:申请内存的大小size
输出:若分配成功,则返回一个内存的地址,否则返回NULL
{
    if(size大于128){ 启动第一级分配器直接调用malloc分配所需的内存并返回内存地址;}
    else {
        将size向上round up成8的倍数并根据大小从free_list中取对应的表头free_list_head;
        if(free_list_head不为空){
              从该列表中取下第一个空闲块并调整free_list;
              返回free_list_head;
        } else {
             调用refill算法建立空闲块列表并返回所需的内存地址;
        }
   }
}
算法: refill
输入:内存块的大小size
输出:建立空闲块链表并返回第一个可用的内存块地址
{
     调用chunk_alloc算法分配若干个大小为size的连续内存区域并返回起始地址chunk和成功分配的块数nobj;
    if(块数为1)直接返回chunk;
    否则
    {
         开始在chunk地址块中建立free_list;
         根据size取free_list中对应的表头元素free_list_head;
         将free_list_head指向chunk中偏移起始地址为size的地址处, 即free_list_head=(obj*)(chunk+size);
         再将整个chunk中剩下的nobj-1个内存块串联起来构成一个空闲列表;
         返回chunk,即chunk中第一块空闲的内存块;
     }
}

算法:chunk_alloc
输入:内存块的大小size,预分配的内存块块数nobj(以引用传递)
输出:一块连续的内存区域的地址和该区域内可以容纳的内存块的块数
{
      计算总共所需的内存大小total_bytes;
      if(内存池中足以分配,即end_free - start_free >= total_bytes) {
          则更新start_free;
          返回旧的start_free;
      } else if(内存池中不够分配nobj个内存块,但至少可以分配一个){
         计算可以分配的内存块数并修改nobj;
         更新start_free并返回原来的start_free;
      } else { //内存池连一块内存块都分配不了
         先将内存池的内存块链入到对应的free_list中后;
         调用malloc操作重新分配内存池,大小为2倍的total_bytes加附加量,start_free指向返回的内存地址;
         if(分配不成功) {
             if(16个空闲列表中尚有空闲块)
                尝试将16个空闲列表中空闲块回收到内存池中再调用chunk_alloc(size, nobj);
            else {
                   调用第一级分配器尝试out of memory机制是否还有用;
            }
         }
         更新end_free为start_free+total_bytes,heap_size为2倍的total_bytes;
         调用chunk_alloc(size,nobj);
    }
}

算法:deallocate
输入:需要释放的内存块地址p和大小size
{
    if(size大于128字节)直接调用free(p)释放;
    else{
        将size向上取8的倍数,并据此获取对应的空闲列表表头指针free_list_head;
       调整free_list_head将p链入空闲列表块中;
    }
}

假设这样一个场景,free_list[2]已经指向了大小为24字节的空闲块链表,如图1所示,当用户向allocator申请21字节大小的内存块时,allocaotr会首先检查free_list[2]并将free_list[2]所指的内存块分配给用户,然后将表头指向下一个可用的空闲块,如图2所示。注意,当内存块在链表上是,前4个字节是用作指向下一个空闲块,当分配给用户时,它是一块普通的内存区。
[url=https://z6pukw.bay.livefilestore.com/y1mHT1YD7RMHHg_uOBBlqYTgXTGgL7xe_9q3AhnwVpthyZObzZo6B9d6Go2reywMyKm9Gc1D4Fr6MQKHv46sLfoBOhxWtnt3Bgp2PgUvUImaLvH4rILZpR2kmm2zgkYpCZ9_PHyFXu61ss/clip_image002[5].gif][/url]
图1 某时刻allocator的状态
[url=https://z6pukw.bay.livefilestore.com/y1maGO2UDA5VVg6EHiK8wgjb4Sg9pBIkBn1pf1JFt1yJD61Y1scB4q4d5DbJkYvxbAf3akV8wXb7AO4rsVYuxkA2h6thhNI0MqsaNtX0bKjNlW18VoZQnU4OvRQGNzKHDKert2TpIoZzf4/clip_image004[10].gif][/url]
图2 分配24字节大小的内存块
4. 小结
STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。
3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,
但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述:
stl::vector<int> array;
array是知道它需要分配的对象大小为sizeof(int)。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free(p)。

usidc52011-11-01 12:35
注意,这里只是讲如何实现一个“能用的 ”allocator,而不是如何实现一个很高级很牛的allocator。当然,你可以回头自己把它变的很高级很牛。
这里假设你已经知道STL的allocator是做什么用的,如果不知道的话随便打开一个STL容器的代码看一看也能很容易猜到。
话说如果我们只想要一个能工作allocator,那就直接使用STL自带的默认实现好了,但是万一想要自己定制一个,你可能会不知道如何下手,而且你又不想去翻阅c++标准文档,也不想去啃如天书般的STL源码,那就走这条捷径——照抄下面的代码,不需要问为什么。

template<typename Type>
class StlAllocator
{
public:
typedef Type value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
public:
template<typename U>
struct rebind
{
typedef StlAllocator<U> other;
};
public:
inline StlAllocator() {}
inline ~StlAllocator() {}
inline StlAllocator(StlAllocator const&) {}
template<typename U>
inline StlAllocator(StlAllocator<U> const&) {}
inline pointer address(reference r) { return &r; }
inline const_pointer address(const_reference r) { return &r; }
inline pointer allocate( size_type cnt, typename std::allocator<void>::const_pointer = 0 )
{
return malloc( cnt * sizeof(Type) );
}
inline void deallocate( pointer p, size_type size )
{
free( p );
}
inline size_type max_size() const
{
return std::numeric_limits<size_type>::max() / sizeof(Type);
}
inline void construct(pointer p, const Type& t) { new(p) Type(t); }
inline void destroy(pointer p) { p->~Type(); }
inline bool operator==(StlAllocator const&) const { return true; }
inline bool operator!=(StlAllocator const& a) const { return !operator==(a); }
};
需要关注的就是allocate和deallocate两个函数,你可以把里面的malloc和free改成你自己的算法。

然后,你就可以这样使用你的allocator了:
std::vector<t, StlAllocator<t> >

usidc52011-11-01 12:37
一个好的内存分配器(allocator),对于服务器的性能是至关重要的,vc版STL、STLPort、Loki、ACE之类的库都带了内存分配器, 但是它们的实现方法、效率都有所不同,在别人的代码里也看了不少别人写的内存分配器,自己也写过一个内存分配器,它们或多或少都有一些不满足要求、不够灵 活或者效率还可以改善之类的问题,现在在这里对这些内存分配器做一个比较,并为实现一个完善的allocator提供一点帮助。



VC所带的STL中的allocator:最简单的内存分配器,简单的调用new来分配内存,跟直接使用new没什么两样,不过STL中的 allocator本身就是一个模板参数,缺省的allocator只是一个默认实现而已,可以仿照allocator实现自己的内存分配,并用于 list、map之类的容器中

STLPort中的allocator:提供了两种内存分配器,一种为简单的调用new,另一种采用预分配内存池的方式来分配内存,这种方式和loki中 提供的分配器大致相同,但有一些小的区别,它们之间的细节差别,下面会详细的分析到。候捷翻译的《STL源码剖析》一书中详细介绍了StlPort中的内 存分配器实现的原理



ACE:ace的内存分配器提供了多种接口,易用性好,但ace代码结构本身比较庞大,分析起来比较复杂,对于个人实现自己的内存分配器用处不大

Loki:loki是一个短小精悍的库,这里只考虑其中的内存分配器,在候捷翻译的《C++设计新思维》中也论述了loki内存分配器的相关内容。但出书 时loki版本较低,现在loki已经发展到0.1.3版,就内存分配器而言,跟书中所述已经有些出入,接口也变得更加丰富,下面着重分析loki 0.1.3中的内存分配器。

loki的内存分配器一共分为4个层次,从底层到上层依次是Chunk、FixedAllocator、SmallObjAllocator、SmallObj:

Chunk : 管理一个大内存块,其中包含若干相同大小的内存分配单元

FixedAllocator : 固定大小的内存分配器,管理若干Chunk,负责新Chunk的建立以及内存释放时Chunk内存的释放

SmallObjAllocator : 不定大小的内存分配器,管理不同大小的FixedAllocator,可分配不同大小不同的内存块

SmallObj :重载new和delete操作符,使用SmallObjAllocator来分配内存,所有小对象的基类



ACE和loki的分配方案大致上都相同,最重要的一个差别是ACE缓存所有已经分配过的内存,一直都不释放,而loki的作法是当某一个chunk的内 存全部被应用程序归还时,向系统归还这个chunk的内存。很明显,如果两者的效率相同的话,显然后者更优越,可惜世上没有免费的午餐,loki要达到这 个目的,就必须要做以下的运算:


1,归还内存的时候,根据要归还的内存地址和大小,找到该内存所在的chunk,loki的作法是保存最后一次归还操作的chunk作为高速缓存,下一次归还内存时首先判断该内存是否属于此chunk,是的话直接归还,否则要作一个线性搜索,loki 0.1.3中采用的方法是根据此chunk的位置作向前向后搜索,目的是加大搜索的命中率


2,申请内存的时候,也是同样,分配器保存上次分配内存时的chunk,下次分配的时候直接拿该chunk来分配,如果该chunk的内存已经分配完毕,同样需要做一次线性搜索

鉴于当前大多数应用服务器的瓶颈并不在普通内存上,似乎ACE的作法要更好一点,即用单链表保存当前已归还的内存块,如果该链表已空,则向操作系统申请虚拟内存,释放内存时则加入到链表中,这种方法的效率应该是最高的,但是缺点是可能某个瞬间系统内存消耗过大而且永不释放

usidc52011-11-01 12:50
认识一下new和delete的开销:


new和delete首先会转调用到malloc和free,这个大家应该很熟识了。很多人认为malloc是一个很简单的操作,其实巨复杂,它会执行一个系统调用(当然不是每一次,windows上是按页算),该系统调用会锁住内存硬件,然后通过链表的方式查找空闲内存,如果找到大小合适的,就把用户的进程地址映射到内存硬件地址中,然后释放锁,返回给进程。
如果在多线程环境下,进程内的分配也会上锁,跟上面类似,不过不是以页,而是以分配的内存为单位。
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就肯定是系统调用。
当然,有些情况是不可说用什么就用什么的,但如果可选的话,优先使用栈上的对象,其次考虑内存池,然后再考虑系统调用。

usidc52011-11-01 12:57
概要


本文比较各种内存分配器(Allocator)在用于STL容器上的性能。参与本次比较的Allocator有:


普通new/delete (用作性能基准)
AutoFreeAlloc
ScopeAlloc
选择的STL容器为map。其他容器如std::list, std::set, std::multi_set, std::multi_map类似1。


对比程序


测试方法 - 对比以下四种情况:


使用标准的std::map<int, int>。
使用ScopeAlloc作为分配器。即std::Map<int, int>。
使用AutoFreeAlloc作为分配器。即std::Map<int, int, std::less<int>, std::AutoFreeAlloc>。
使用ScopeAlloc作为分配器,且共享同一个std::BlockPool。
测试程序(参见<stdext/Map.h>):


template <class LogT>
class TestMap : public TestCase
{
    WINX_TEST_SUITE(TestMap);
        WINX_TEST(testCompare);
    WINX_TEST_SUITE_END();

public:
    enum { N = 20000 };

    void doStlMap(LogT& log)
    {
        typedef std::map<int, int> MapT;
        log.print("===== std::map =====\n");
        std::PerformanceCounter counter;
        {
            MapT coll;
            for (int i = 0; i < N; ++i)
                coll.insert(MapT::value_type(i, i));
        }
        counter.trace(log);
    }

    void doMap1(LogT& log)
    {
        typedef std::Map<int, int, std::less<int>, std::AutoFreeAlloc> MapT;
        log.print("===== std::Map (AutoFreeAlloc) =====\n");
        std::PerformanceCounter counter;
        {
            std::AutoFreeAlloc alloc;
            MapT coll(alloc);
            for (int i = 0; i < N; ++i)
                coll.insert(MapT::value_type(i, i));
        }
        counter.trace(log);
    }

    void doMap2(LogT& log)
    {
        typedef std::Map<int, int> MapT;
        log.print("===== std::Map (ScopeAlloc) =====\n");
        std::PerformanceCounter counter;
        {
            std::BlockPool recycle;
            std::ScopeAlloc alloc(recycle);
            MapT coll(alloc);
            for (int i = 0; i < N; ++i)
                coll.insert(MapT::value_type(i, i));
        }
        counter.trace(log);
    }

    void doShareAllocMap(LogT& log)
    {
        typedef std::Map<int, int> MapT;
        std::BlockPool recycle;
        log.newline();
        for (int i = 0; i < 5; ++i)
        {
            log.print("===== doShareAllocMap =====\n");
            std::PerformanceCounter counter;
            {
                std::ScopeAlloc alloc(recycle);
                MapT coll(alloc);
                for (int i = 0; i < N; ++i)
                    coll.insert(MapT::value_type(i, i));
            }
            counter.trace(log);
        }
    }

    void testCompare(LogT& log)
    {
        for (int i = 0; i < 5; ++i)
        {
            log.newline();
            doStlMap(log);
            doMap2(log);
            doMap1(log);
        }
        doShareAllocMap(log);
    }
};
测试结果


===== std::map =====
---> Elapse 29649998 ticks (8283.18 ms) (0.14 min) ...
===== std::Map (ScopeAlloc) =====
---> Elapse 9045729 ticks (2527.06 ms) (0.04 min) ...
===== std::Map (AutoFreeAlloc) =====
---> Elapse 9683232 ticks (2705.16 ms) (0.05 min) ...


===== std::map =====
---> Elapse 38099194 ticks (10643.59 ms) (0.18 min) ...
===== std::Map (ScopeAlloc) =====
---> Elapse 15013312 ticks (4194.20 ms) (0.07 min) ...
===== std::Map (AutoFreeAlloc) =====
---> Elapse 15039592 ticks (4201.54 ms) (0.07 min) ...


===== std::map =====
---> Elapse 24698608 ticks (6899.93 ms) (0.11 min) ...
===== std::Map (ScopeAlloc) =====
---> Elapse 10867094 ticks (3035.89 ms) (0.05 min) ...
===== std::Map (AutoFreeAlloc) =====
---> Elapse 15611309 ticks (4361.26 ms) (0.07 min) ...


===== std::map =====
---> Elapse 37594376 ticks (10502.56 ms) (0.18 min) ...
===== std::Map (ScopeAlloc) =====
---> Elapse 8846240 ticks (2471.33 ms) (0.04 min) ...
===== std::Map (AutoFreeAlloc) =====
---> Elapse 8667671 ticks (2421.44 ms) (0.04 min) ...


===== std::map =====
---> Elapse 30175306 ticks (8429.93 ms) (0.14 min) ...
===== std::Map (ScopeAlloc) =====
---> Elapse 9033720 ticks (2523.71 ms) (0.04 min) ...
===== std::Map (AutoFreeAlloc) =====
---> Elapse 8723023 ticks (2436.91 ms) (0.04 min) ...


===== doShareAllocMap =====
---> Elapse 8441194 ticks (2358.18 ms) (0.04 min) ...
===== doShareAllocMap =====
---> Elapse 7651772 ticks (2137.64 ms) (0.04 min) ...
===== doShareAllocMap =====
---> Elapse 7900730 ticks (2207.19 ms) (0.04 min) ...
===== doShareAllocMap =====
---> Elapse 7861035 ticks (2196.10 ms) (0.04 min) ...
===== doShareAllocMap =====
---> Elapse 7530844 ticks (2103.86 ms) (0.04 min) ...
测试结论


使用ScopeAlloc或者AutoFreeAlloc后,STL容器的性能有显著提高。而ScopeAlloc与AutoFreeAlloc在该容器存在大量内存分配时区别不显著2。另外,由于内存池技术的作用,ScopeAlloc在Share同一个BlockPool后,性能略有改善。


Footnotes
1. 请注意我这里没有包含std::vector, std::deque, std::stack(其实也是std::deque)等。这几个容器并不需要一个特别的Allocator,普通的new/delete足矣。关于这一点,在MemPool释义中亦有说明。
2. 关于AutoFreeAlloc与ScopeAlloc的性能对比,参阅《各种内存分配器的性能对比》。

usidc52011-11-01 13:01
概要


本文比较各种内存分配器(Allocator)的性能。参与本次比较的Allocator有:


普通new/delete (用作性能基准)
AutoFreeAlloc
ScopeAlloc
对比一:单个Allocator实例仅申请少量的小块内存


测试方法:单个Allocator仅申请一个int,对比其速度。


测试程序(参见<stdext/Memory.h>):


template <class LogT>
class TestCompareAllocators : public TestCase
{
    WINX_TEST_SUITE(TestCompareAllocators);
        WINX_TEST(testComparison1);
    WINX_TEST_SUITE_END();

public:
    enum { N = 60000 };

    void doNewDelete1(LogT& log)
    {
        log.print("===== NewDelete =====\n");
        PerformanceCounter counter;
        for (int i = 0; i < N; ++i)
        {
            int* p = new int;
            delete p;
        }
        counter.trace(log);
    }

    void doAutoFreeAlloc1(LogT& log)
    {
        log.print("===== AutoFreeAlloc =====\n");
        PerformanceCounter counter;
        for (int i = 0; i < N; ++i)
        {
            AutoFreeAlloc alloc;
            int* p = STD_NEW(alloc, int);
        }
        counter.trace(log);
    }

    void doScopeAlloc1(LogT& log)
    {
        log.print("===== ScopeAlloc =====\n");
        BlockPool recycle;
        PerformanceCounter counter;
        for (int i = 0; i < N; ++i)
        {
            ScopeAlloc alloc(recycle);
            int* p = STD_NEW(alloc, int);
        }
        counter.trace(log);
    }

    void testComparison1(LogT& log)
    {
        for (int i = 0; i < 4; ++i)
        {
            log.newline();
            doAutoFreeAlloc1(log);
            doNewDelete1(log);
            doScopeAlloc1(log);
        }
    }
};
测试结果:


===== AutoFreeAlloc =====
---> Elapse 283773 ticks (79.28 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 134936 ticks (37.70 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 8285 ticks (2.31 ms) (0.00 min) ...


===== AutoFreeAlloc =====
---> Elapse 184090 ticks (51.43 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 116476 ticks (32.54 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 9831 ticks (2.75 ms) (0.00 min) ...


===== AutoFreeAlloc =====
---> Elapse 179652 ticks (50.19 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 130553 ticks (36.47 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 8387 ticks (2.34 ms) (0.00 min) ...


===== AutoFreeAlloc =====
---> Elapse 137255 ticks (38.34 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 103869 ticks (29.02 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 8215 ticks (2.29 ms) (0.00 min) ...
测试结论:


在单个Allocator仅申请少量内存时,AutoFreeAlloc性能最差,new/delete次之,ScopeAlloc最好。


对比二:单个Allocator实例申请大量的小块内存


测试方法:单个Allocator申请6万个int,对比其速度。


测试程序(参见<Memory.h>):


template <class LogT>
class TestCompareAllocators : public TestCase
{
    WINX_TEST_SUITE(TestCompareAllocators);
        WINX_TEST(testComparison2);
    WINX_TEST_SUITE_END();

public:
    enum { N = 60000 };

    void doNewDelete2(LogT& log)
    {
        int i, *p[N];
        log.print("===== NewDelete =====\n");
        PerformanceCounter counter;
        for (i = 0; i < N; ++i)
        {
            p = new int;
        }
        for (i = 0; i < N; ++i)
        {
            delete p;
        }
        counter.trace(log);
    }

    void doAutoFreeAlloc2(LogT& log)
    {
        log.print("===== AutoFreeAlloc =====\n");
        PerformanceCounter counter;
        {
            AutoFreeAlloc alloc;
            for (int i = 0; i < N; ++i)
            {
                int* p = STD_NEW(alloc, int);
            }
        }
        counter.trace(log);
    }

    void doScopeAlloc2(LogT& log)
    {
        log.print("===== ScopeAlloc =====\n");
        BlockPool recycle;
        PerformanceCounter counter;
        {
            ScopeAlloc alloc(recycle);
            for (int i = 0; i < N; ++i)
            {
                int* p = STD_NEW(alloc, int);
            }
        }
        counter.trace(log);
    }

    void testComparison2(LogT& log)
    {
        for (int i = 0; i < 4; ++i)
        {
            log.newline();
            doAutoFreeAlloc2(log);
            doNewDelete2(log);
            doScopeAlloc2(log);
        }
    }
};
测试结果:


===== AutoFreeAlloc =====
---> Elapse 1771 ticks (0.49 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 117791 ticks (32.91 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 2657 ticks (0.74 ms) (0.00 min) ...


===== AutoFreeAlloc =====
---> Elapse 1637 ticks (0.46 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 114280 ticks (31.93 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 2516 ticks (0.70 ms) (0.00 min) ...


===== AutoFreeAlloc =====
---> Elapse 1668 ticks (0.47 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 118661 ticks (33.15 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 2553 ticks (0.71 ms) (0.00 min) ...


===== AutoFreeAlloc =====
---> Elapse 1678 ticks (0.47 ms) (0.00 min) ...
===== NewDelete =====
---> Elapse 113231 ticks (31.63 ms) (0.00 min) ...
===== ScopeAlloc =====
---> Elapse 3287 ticks (0.92 ms) (0.00 min) ...
测试结论:


在单个Allocator申请大量的小块内存时,AutoFreeAlloc性能最好,ScopeAlloc次之(但和AutoFreeAlloc很接近,差异不显著),new/delete最差。


总体结论


结论1:


生成一个新的AutoFreeAlloc实例是一个比较费时的操作,其用户应注意做好内存管理的规划。而生成一个ScopeAlloc实例的开销很小,你甚至可以哪怕为生成每一个对象都去生成一个ScopeAlloc都没有关系(当然我们并不建议你这样做)。


结论2:


AutoFreeAlloc有较强的局限性,仅仅适用于有限的场合(局部的复杂算法);而ScopeAlloc是通用型的Allocator,基本在任何情况下,你都可通过使用ScopeAlloc来进行内存管理,以获得良好的性能回报。


Hide All Comments | Unfold All | Fold All
Fold
测试环境
winxgui 22 Jan 2008, 10:48 GMT+1030
CPU:1.2 G
操作系统:Windows XP
编译器:Visual C++ 6.0
优化选项:Maximize speed(最大速度)
C库:Multithreaded DLL
配置:Release版本
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值