STL源码分析:空间配置器浅析

       

目录

allocator

alloc

一级配置器

二级配置器

自由链表

内存分配allocate

_S_refill函数

_S_chunk_alloc函数

内存释放deallocate

为什么要使用free_list?

为什么free_list要把128bytes分成16部分?


对于一些容器如vector、map、set之类的,都需要一个模板参数Alloc,用来指定一个空间配置器,这个配置器的作用,就是管理内存的分配和释放。在SGI STL中,提供了默认的配置器,这也是为什么我们在创建一个vector、map的时候不需要传入第二个参数。以vector为例,其头部定义如下:

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc> 
{...}

       这里指定了一个默认的配置器参数, __STL_DEFAULT_ALLOCATOR(_Tp)。 这是一个宏,它有两种情况:

# ifndef __STL_DEFAULT_ALLOCATOR
#   ifdef __STL_USE_STD_ALLOCATORS
#     define __STL_DEFAULT_ALLOCATOR(T) allocator< T >
#   else
#     define __STL_DEFAULT_ALLOCATOR(T) alloc
#   endif
# endif

      也就是说,默认的配置器要么是allocator,要么就是alloc。

allocator

      当然这里的allocator并不是本文的重点,因为它只是把operator new和operator delete简单进行了封装,如下所示:

template <class T>
inline T* allocate(ptrdiff_t size, T*) {//进行内存的分配,并且返回分配内存的首地址
    set_new_handler(0);
    T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
    if (tmp == 0) {
	cerr << "out of memory" << endl; 
	exit(1);
    }
    return tmp;
}


template <class T>
inline void deallocate(T* buffer) {//delete释放资源
    ::operator delete(buffer);
}

template <class T>
class allocator {
public:
    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;
    pointer allocate(size_type n) { 
	return ::allocate((difference_type)n, (pointer)0);
    }
    void deallocate(pointer p) { ::deallocate(p); }
    pointer address(reference x) { return (pointer)&x; }
    const_pointer const_address(const_reference x) { 
	return (const_pointer)&x; 
    }
    size_type init_page_size() { 
	return max(size_type(1), size_type(4096/sizeof(T))); 
    }
    size_type max_size() const { 
	return max(size_type(1), size_type(UINT_MAX/sizeof(T))); 
    }
};

       allocator的内容很简单,下面着重来看看alloc。

alloc

        alloc是一个经过typedef定义的新类型,其定义如下:

typedef __malloc_alloc_template<0> malloc_alloc;

# ifdef __USE_MALLOC

typedef malloc_alloc alloc;
...
#else typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
...
#endif

       从这里可以看到,alloc的类型实际上是由__USE_MALLOC这个宏来定义的,如果定义了,那么alloc使用的就是__malloc_alloc_template,否则就是__default_alloc_template。

       这两个都是类,前者就是一级配置器,后者是二级配置器。二者都包含了基本的内存分配、内存释放等函数。

一级配置器

       一级配置器是__malloc_alloc_template类,其定义如下:

template <int __inst>
class __malloc_alloc_template {

private:

  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
  static void (* __malloc_alloc_oom_handler)();
#endif

public:

  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n); //直接用malloc进行分配,如果分配失败调用_S_oom_mallo
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }

  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p); //释放
  }

  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz); //用realloc分配,分配失败调用_S_oom_realloc
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }

  static void (* __set_malloc_handler(void (*__f)()))()//设置异常捕捉函数,并且返回设之前老的捕捉函数
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }

};

       一级配置器其实很简单,它内部的allocate、deallocate和reallocate函数实际上就是对malloc、free和realloc的简单封装,还提供了一个设置异常捕捉函数的接口__set_malloc_handler,这个函数的作用就是用传入的函数指针更新类静态变量函数指针__malloc_alloc_oom_handler并且返回更新前的__malloc_alloc_oom_handler。

       那么__malloc_alloc_oom_handler这个函数指针有什么用呢?

       在allocate和reallocate函数中,如果内存分配失败,就会调用相应的_S_oom_alloc和_S_oom_realloc,它们的定义如下:

template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) {//循环调用malloc,直到分配成功,分配失败就调用异常捕捉函数
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*__my_malloc_handler)();
        __result = malloc(__n);
        if (__result) return(__result);
    }
}

template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) {
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*__my_malloc_handler)();
        __result = realloc(__p, __n);
        if (__result) return(__result);
    }
}

       二者的作用都是类似的:当内存分配失败时调用这两个函数,在这两个函数内部,会调用__malloc_alloc_oom_handler指向的异常捕捉函数,经过异常捕捉函数处理后,如果顺利返回,就会再次调用内存分配函数进行分配,如果内存分配仍然失败,那么就会进入下一次循环,再次调用异常捕捉函数进行处理。

       关于异常捕捉函数,用户是可以通过前面的__set_malloc_handler来自定义的(类似于设置new函数的new_handler),在《Effective C++ 》item 49中提到,这类new_handler的行为,有以下选择:

1.让更多内存可被使用。既然内存分配失败,说明当前没有可用的内存,因此new_handler需要采取相应的措施来获得更多的内存空间;

2.安装另一个handler。由于多次循环调用的异常处理函数是可以不同的,因此可以在当前的异常处理函数中安装另一个handler,如果当前的new handler没有能力让更多内存供使用,那么就可以安装一个有能力的handler;

3.卸除handler。卸除handler相当于让__malloc_alloc_oom_handler为0,从程序中可以看到,卸除后如果再次发生分配失败情况,就会直接抛出bad alloc的异常;

4.抛出异常。可以直接在异常处理函数中抛出ban_alloc的异常;

5.不返回。通过exit或者abort结束程序。

       对于一级配置器来说,和前面的allocator不同,allocator是简单的封装了operator new和operator delete,而这里的一级配置器则是简单封装的malloc和free,并且提供了类似于new handler的“malloc handler”

二级配置器

       一级配置器虽然简单,但是如果分配的区块很小,那么就很容易造成内存碎片,这种碎片是外部碎片。并且这样的小型区块越多造成的内存碎片就越严重,二级配置器就是用于解决这一问题的。

       二级配置器会视情况作出不同决策:如果需要分配的区块大小超过128bytes,那么就认为这是大区块,就直接使用一级配置器进行分配;如果大小不足128bytes,那么就认为这是小区块,就使用内存池方式来进行分配。

       对于使用内存池的情况,二级配置器的思路是:每次小区块需要分配的内存都从内存池中去获取,如果内存池已经没有空间了,就会重新在堆上为内存池申请一片较大的空间,以供后续分配,通过内存池的方式,可以明显减少malloc的调用次数,用集中的内存池空间来取代原来分散的空间,如此来尽量避免内存碎片的产生

       二级配置器是__default_alloc_template类,它把内存池空间分作两部分,一部分是放在自由链表上的已从内存池中取出但还未分配出去,另一部分则是内存池中未被取出的剩余空间

      __default_alloc_template用三个静态变量来描述内存池:

  static char* _S_start_free;   //当前内存池剩余空间的起始地址
  static char* _S_end_free;    //当前内存池剩余空间的末尾
  static size_t _S_heap_size;   //整个已申请的多个内存池总大小

       需要注意的是这里的_S_heap_size,是指内存池总大小,它不仅包含当前的内存池,如果当前内存池大小不足,那么就还会申请一个新的内存池,而这个变量是会记录所有申请的内存池的总大小的。

自由链表

       先来说说自由链表是什么。

       前面说了,使用内存池的情况,是分配的区块小于128bytes,STL中把这128按照每8个字节分成一组,分了16组,分别对应8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128bytes。自由链表结构如下:

       自由链表有以下特点:

1.本身是一个大小为16的数组_S_free_list,数组元素类型为obj *;

2.每一个元素中包含了一个obj结点的地址,指向一个链表;

3.指向的链表由一系列obj类型的结点构成,每个结点都是一个内存块,同一链表中的内存块大小都相同;

4.所有链表上的内存块都是从内存池中取出,但是还没有真正分配出去

5.如果需要分配8个字节大小的内存块,就返回_S_free_list[0]对应的链表中的第一个结点,然后该链表把这个结点淘汰出去,第二个结点成为新的结点;如果需要分配16个字节,就对应_S_free_list[1];分配24、32、...、128以此类推;

       到这里简单的说了一下自由链表的特点和结构,如前所述,自由链表里的各个链表结点都是obj类型,该类型定义如下:

union _Obj {
        union _Obj* _M_free_list_link;
        char _M_client_data[1];    /* The client sees this.        */
  };

        这是一个联合体,也就是说,每条链表中的每个结点都是一个联合体_Obj类型,它其中包含了两个成员:指向另一个联合体_Obj类型的指针_M_free_list_link,和一个1字节的char数组_M_client_data。

        _M_free_list_link很好理解,就是一个指向同类型的指针,它就是链表结点与结点之间的“后驱指针”。

        而至于_M_client_data,虽然这里定义了但是整个STL中都没有用到,根据书中所说,_M_client_data相当于是一个指向实际区块的指针,但是个人觉得本身分配实际区块时返回的就是其地址,难道谁还会通过_M_client_data去获取地址么?显然不够使人信服,查过一些其它资料但是都莫衷一是,既然STL中没有用到,无法直观体会其妙处,因此这里就不做过多研究了。(如果读者有比较好的解释,希望留言指教)。

内存分配allocate

       自由链表的每个元素都对应了内存大小为8的倍数的内存块所组成的链表,但是传入的需要分配的大小往往不是8的倍数,举个例子,如果需要用allocate分配大小为1~8bytes的空间,那么显然就应该分配大小为8字节的内存块,如果需要分配大小为9~16的空间,就应该分配大小为16的内存块。因此,对于传入的参数__n,需要先将其转换到不小于__n的最小的8的倍数,然后分配该数值对于的内存块。allocate函数定义如下:

static void* allocate(size_t __n)
  {
    void* __ret = 0;

    if (__n > (size_t) _MAX_BYTES) {//如果超过128字节
      __ret = malloc_alloc::allocate(__n);//直接使用一级配置器来分配
    }
    else {
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n); //_S_freelist_index找到__n对应的在数组的元素索引,__my_free_list就是数组上的那个元素,这个元素所指向的链表中结点都是大小为__n上调至8的倍数的内存块
      // Acquire the lock here with a constructor call.
      // This ensures that it is released in exit or during stack
      // unwinding.
#     ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance; //加锁,析构时自动解锁
#     endif
		//__my_free_list就是大小为__n对应的链表所在数组中的那个元素地址
		//数组的每个元素都是一个_Obj *类型,* __my_free_list取得该obj指针
      _Obj* __RESTRICT __result = *__my_free_list;//__result保存第一个obj结点的地址
      if (__result == 0)//说明__my_free_list指向一个null的obj结点
        __ret = _S_refill(_S_round_up(__n));
      else {
        *__my_free_list = __result -> _M_free_list_link; //把下一个obj结点地址放到数组中
        __ret = __result;
      }
    }

    return __ret;//返回obj结点的地址
  };

该函数主要做了以下几件事:

1.如果需要分配的空间大小超过128,就认为是大区块,使用一级配置器进行分配;

2.如果使用二级配置器,先找到不小于传入的参数__n的最小的8的倍数设为m,然后找到大小为m的区块所在链表对应自由链表数组的元素(如分配22bytes的空间就要找到_S_free_list[2]);

3.找到相应的数组元素后,由于该元素存放的就是对应链表的头结点地址,取出元素值,如果为0说明自由链表中不存在多余的大小为m的内存块了,此时就应该从内存池中去申请内存块;如果不为0,说明自由链表中存在大小为m的内存块,就将这个内存块结点从链表中抽出,第二个内存块作为新的头结点,返回抽出的内存块的地址。

       可以看到,allocate函数返回的就是分配的内存块的地址,第1步自然不用说,第2步需要对传入的参数__n进行处理,通过_S_freelist_index函数实现:

static  size_t _S_freelist_index(size_t __bytes) {
        return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);//找到能存放下__bytes个字节的那一条链表
  }

       该函数返回的是free_list的索引。

       第3步发生的情况是,找到了__n对应的free_list元素,但是这个元素下面没有内存块组成的链表,这就说明自由链表中没有相应大小的内存块,此时就需要到内存池中去申请内存块了。如下所示:

_S_refill函数

       比如说一次要分配8个字节的内存,二级配置器并不会只是从内存池中取出8个字节,而是取出n*8的大小,然后把这n*8的大小分成n块,每块8个字节,把这n块连接起来成为链表,把第一块的地址放在自由链表中8字节对应的位置。这样,以后每次需要分配8个字节内存时,直接从自由链表上8字节对应的位置,找到对应的链表,返回第一个结点地址即可。这里所说的n,默认值为20,这一点在_S_refill函数中有所体现。

template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    char* __chunk = _S_chunk_alloc(__n, __nobjs);//向内存中申请20个大小为n的块,实际上是申请大小为20*__n的连续空间,实际上分配的块数保留在__nobjs中
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    if (1 == __nobjs) return(__chunk);//如果只分配了一个块,那么就直接返回这个块的地址

	//如果分配了多个大小为__n的块(这些块的地址都是连续的,就要把这些块链起来,放到free_list下面的链表中
    __my_free_list = _S_free_list + _S_freelist_index(__n);//找到这个块大小对应的free_list的位置

    /* Build free list in chunk */
      __result = (_Obj*)__chunk;//先记录下分配的块的首地址
      *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);//__chunk偏移__n就是下一个块的地址,把下一个块的地址保存在__next_obj中,并且放在free_list下链表的头部
      for (__i = 1; ; __i++) {//从__newxt_obj开始,依次把这些块链起来
        __current_obj = __next_obj;
        __next_obj = (_Obj*)((char*)__next_obj + __n);
        if (__nobjs - 1 == __i) {//已经到最后一个块了
            __current_obj -> _M_free_list_link = 0;//最后一个块的后驱为0
            break;
        } else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
      }
    return(__result);
}

该函数做了以下几件事:

1.通过_S_chunk_alloc函数从内存池中申请20个大小为n的内存块,实际申请的内存块数为nobjs;

2.如果只申请了一个块,那么直接返回这个块的首地址即可;

3.如果申请了nobjs个块(nobjs>1),先找到n大小的块对应在free_list的位置,然后把这nobjs个块一个接一个的用后驱指针链成一个链表,末尾结点的后驱指针为0,返回首结点地址。

     由此可见,_S_refill的作用,是从内存池中申请多个大小为__n的内存块放到自由链表中的相应位置。而具体的申请,则是通过_S_chunk_alloc函数完成的。

_S_chunk_alloc函数

       由于_S_refill每次申请时并不只是申请一个区域块,这也就使得每次从内存池中申请内存时都会告诉内存池两个信息:需要申请的块数nobjs和每一个块的大小n。此时就会出现以下几种情况:

1.内存池中剩余大小足以分配n*nobjs,那么直接返回内存池中剩余空间的首地址,并且偏移n*nobjs;

2.内存池中剩余大小不足以分配nobjs个大小为n的块,但是至少可以分配1个大小为n的块,那么就能分配多少个大小为n的块就分配多少个,然后返回剩余空间首地址后进行相应的偏移;

3.内存池中剩余大小连一个大小为n的块都分配不了,那么就要考虑重新从堆上分配空间给内存池了,由于分配后的内存池必定是一个全新的内存池,所以在重新分配之前应该把当前内存池中剩余空间利用起来,把剩余空间放到自由链表上(由于内存池大小必定为8的倍数,而每次从内存池中取出的大小也是8的倍数,所以剩余的空间也必定是8的倍数,所以就不会出现7个字节接到8个字节的链表,12个字节接到16个字节这样类似的情况)。

    接着分配一个新的内存池。如果分配成功,那就很好办,重新用nobjs和n向新分配的内存池申请空间即可。

    如果此时分配失败,那么就试着在已经挂在自由链表的那一部分空间中找出是否还有大于n的块还未分配的,如果有,就把大于n的这个块的首地址返回。这个过程就是:从自由链表上n+8对应的结点开始遍历(不能从头开始,因为比n小的结点是放不下n的),查看是否有结点对应的链表不为空,不为空说明还有空间可以用来分配大小为n的空间,就把这个空间的首地址返回。不管是否能找到更大的空间来分配一个n大小的块,都会试着调用一级配置器中的allocate函数,此时要么就是最差的情况:实在没有办法找到空间来分配,那么allocate函数势必会抛出异常,要么就是allocate函数能够改善内存不足的情况。

      二级配置器中就是使用_S_chunk_alloc函数来向内存池中申请nobjs个大小为n的空间,其定义如下所示:

template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, 
                                                            int& __nobjs)
{
    char* __result;
    size_t __total_bytes = __size * __nobjs;  //总共需要分配的大小
    size_t __bytes_left = _S_end_free - _S_start_free;   //内存池的剩余大小

    if (__bytes_left >= __total_bytes) {  //如果内存池的剩余空间足够
        __result = _S_start_free;  //start_free就是实际分配的首地址
        _S_start_free += __total_bytes;   //相当于内存池空间减少
        return(__result);  //返回分配空间首地址
    } else if (__bytes_left >= __size) {  //内存池的剩余空间不足装下所有,但是至少能分配一个节点
        __nobjs = (int)(__bytes_left/__size);//计算能分配的结点数目
        __total_bytes = __size * __nobjs;//实际分配空间大小
        __result = _S_start_free;//实际分配空间的首地址
        _S_start_free += __total_bytes;//相当于内存池空间减少已分配的大小
        return(__result);
    } else {//内存池的剩余空间连一个节点都不够分配
        size_t __bytes_to_get = 
	  2 * __total_bytes + _S_round_up(_S_heap_size >> 4); //一次性多分配一些
        // Try to make use of the left-over piece.
        if (__bytes_left > 0) { //如果内存池还有剩余的空间
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);//把内存池剩余的空间也利用起来

            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;//_S_start_free相当于内存池剩余的空间的首地址,把剩下的空间作为一个结点插到free_list的头部
            *__my_free_list = (_Obj*)_S_start_free;
        }
        _S_start_free = (char*)malloc(__bytes_to_get);//重新分配
        if (0 == _S_start_free) {//分配失败
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
	    _Obj* __p;
            // Try to make do with what we have.  That can't
            // hurt.  We do not try smaller requests, since that tends
            // to result in disaster on multi-process machines.
            for (__i = __size;
                 __i <= (size_t) _MAX_BYTES;
                 __i += (size_t) _ALIGN) {//从__size(已经成为了8的倍数)对应的节点开始遍历free_list上的所有节点
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;//获取第一个大小为__i的obj
                if (0 != __p) {//__p不为0说明找到了一个比__size大的节点,就把这个obj放到内存池中
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;//把这个obj放到内存池中
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));//此时内存池中必定能放下__size大小了,就再次调用chunk
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }//到这里说明内存池中无论如何都找不到__size的空间了,就只能重新分配了
	    _S_end_free = 0;	// In case of exception.
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);//重新分配大小
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }
        _S_heap_size += __bytes_to_get;//记录内存池的总大小
        _S_end_free = _S_start_free + __bytes_to_get;
        return(_S_chunk_alloc(__size, __nobjs));
    }
}

       当内存池中剩余空间连一个内存块都不够时,会用__bytes_to_get 来计算需要分配的新的内存池的大小。

       即size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);

       其中的__total_bytes是nobjs * n的总大小,这个大小乘以2,然后还有一个附加量。乘以2很好理解,还有一个附加量,这个附加量会随分配次数的增加而增大(相当于一旦出现内存池不足的情况,那么附加量就会上一次分配内存池时的附加量大),这里使用了一个_S_round_up函数,该函数作用是把参数提升到8的倍数。其定义如下:

static size_t
  _S_round_up(size_t __bytes) //提升到8的倍数
    { return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1));

       这样就能保证内存池的大小必定为8的倍数,有利于提高内存池的利用率(在前面分析的_S_chunk_alloc进行的第3步行为中有描述)。

内存释放deallocate

       该函数定义如下:

static void deallocate(void* __p, size_t __n) //需要指明需要释放的空间的大小n
  {
    if (__n > (size_t) _MAX_BYTES)
      malloc_alloc::deallocate(__p, __n);//如果大于128就用一级空间配置器
    else { //不超过128就是用二级空间配置器
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);  //根据n找到节点数组中相应的元素
      _Obj* __q = (_Obj*)__p; //

      // acquire lock
#       ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#       endif /* _NOTHREADS */
      __q -> _M_free_list_link = *__my_free_list;  //相当于把需要删除的obj结点还回到链表头部
      *__my_free_list = __q;
      // lock is released here
    }
  }

      可以看到,最重要的一点,释放空间时又会把它归还到内存池中。deallocate函数需要指定释放的空间大小n,根据这个大小来找到它需要归还的目的位置,然后把这个被释放的内存块插到相应链表的头部即可。

为什么要使用free_list?

       使用内存池的一个简单方法就是记录当前内存池剩余空间的首地址,需要取出多大的空间就取出后让这个地址偏移多少,这样不管是取1个2个4个8个字节都是可以的,并且还比自由链表这种方式更为便捷。但是这种方式最大的问题是内存块释放之后无法归还

       好的内存池设计必然是“有借有还”的,而free_list就是这样。free_list的每个元素都对应了一种固定大小的内存块链表,通过需要释放的内存块的大小,就很容易能找到这个内存块需要归还的位置,然后直接将其添加到相应链表中即可。

        而“有借无还”的内存池,每次借出空间都是无规律的,需要多少就借出多少,可能连着的两个内存块1个是1字节而另一个是100个字节,如果你要让这些借出去的内存块知道归还的位置,就不得不维护其它的数据结构来记录分配出去的内存块的位置,这并不一定就比free_list简单。

为什么free_list要把128bytes分成16部分?

      这里有两个问题,一个是为什么要分,另一个是为什么要分成16部分。

      先来说下为什么要分。上面说了为什么要使用free_list,因为free_list的一个重要特点就是可以根据需要分配/释放的内存块大小来找到相应的位置,如果不分,那就相当于从内存池中取出来的所有内存块只有一种,为了满足128bytes的条件,自然这种内存块就是128bytes大小的了,这样一来,如果你只需要分配1字节,那么内存池实际分配给你的也是128bytes,这样就造成了内存碎片,而且是内部碎片。

       为了避免内存碎片,自然是让free_list适应各种大小。那么要不把内存池分成128种内存块?每一字节都对应一种?这样就违背了内存对齐的原则。因为整个内存池也是从堆中分配的,那么内存池的首地址必然是边界对齐的,如果内存池中的内存块存在各个字节的情况,那么内存池内部分出去的就显然不是内存对齐的了,这样带来的后果就是访存变慢,或者直接无法运行。

       因此,为了保证内存对齐,索性把内存池中的内存块按照8来区分,这样不管如何分配,分配出去的内存块必定都是边界对齐的,有考虑到要尽量适应多种大小的内存块,所以就把128bytes以内的大小分成了8 16 24...128按8递增一共16种大小的内存块,这也就使得free_list数组大小为16,与这16种内存块一一对应。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章