std::allocator——以GNU2.9为例

本文详细解析了GNU2.9标准分配器std::alloc的运行模式,包括一级分配器和二级分配器的实现原理,重点剖析了chunk_alloc()和refill()函数,以及std::alloc如何通过内存池提高内存利用率。文章深入探讨了内存分配策略和碎片管理技术。



1. std::allocator 实现

VC6.0 的 malloc() 内存块布局,由debug header(内含cookie(4bytes))、block size(client所获得pointer指向该块头部)、debug tail、pad(字节填充,使得整个所分配内存块字节数大小为16的倍数)和cookie(共两个cookie,记录该内存块必要信息);

VC6.0、BC5 和 GNU2.9 标准分配器 allocator 实现均是以 ::operator new() 和 ::operator delete() 来完成 allocate() 和 deallocate(), 没有进行任何特殊设计;

其中 GNU2.9 容器使用的不是 std::allocator 而是 std::alloc,对于 GNU2.9 std::alloc 所对应的 GNU4.9 中的分配器为 __pool_alloc (沿用私有继承继承基类的实现,继承自__pool_alloc_base);

GNU4.9 标准分配器的 allocator 只是以 ::operator new() 和 ::operator delete() 完成 allocate() 和 deallocate(),而没有任何特殊设计,且 std::allocator 继承自 __gnu_cxx::new_allocator;



2. GNU2.9 std::alloc 运行模式

由一个单向链表 free_list[16] 对所请求得到的内存块进行管理,其中 free_list[0] 负责 1~8 bytes 的内存块分配,free_list[1] 负责 9~16 bytes 的内存块分配,依此类推,free_list[15] 负责 121~128 bytes 内存块的分配。


① 向计算机申请额外的内存空间
初始化 free_list 时,链表节点均为空节点,即当前大小区间的内存空间尚未分配得到。若是需要分配某一区间所负责大小的内存空间,且对应的链表节点指向为空,则向内存中申请 20 ∗ 2 = 40 20*2 = 40 20∗2=40 块该大小的内存空间,其中 20 20 20块(编程经验值?) 大小作为 cookie free blocks(不含cookie的未分配内存块),并取其一给 客户端使用,余下的 20 20 20 块则作为 内存池(memory pool) 供下次需要不同大小的内存时进行内存划分(由start_free和end_free两个指针所区隔)。

需要注意的是,每次内存划分时所划分的字节数应当是 16 16 16 的倍数,所以每次申请得到的供 std::alloc 调配的内存空间的实际大小应当是 每 个 内 存 块 大 小 ∗ 20 ∗ 2 + R o u n d U p ( 累 计 所 管 理 内 存 块 大 小 > > 4 ) 每个内存块大小*20*2+RoundUp(累计所管理内存块大小>>4) 每个内存块大小∗20∗2+RoundUp(累计所管理内存块大小>>4)

其中还运用到了内嵌指针(embedded pointers)的概念(所分配空间空间最小为8 bytes,大于指针所需空间大小)。在未分配的 cookie free blocks 的头部使用指针指向该内存空间(即memory pool),若是该内存空间被客户取用即被分配,则将指针指向下一同样大小内存块,将原先所指的空间送给客户端使用。所以在 allocator 的实现当中会有 union 联合体(相关内容见:Union类型的使用方法)的身影,这也是 cookie free 和提高空间利用率的一种努力。


② 使用内存池当中的余量
上述所说,若是需要分配的空间大小对应的 free_list[] 节点为空,则申请内存是指 内存池(memory pool)没有余量可满足该次分配的情况下的动作。若是 内存池有足够的空间可以满足该次分配请求,则从内存池中划分出 1~20块 所需大小的内存块(每次向链表进行内存分配时都是以1~20的块数量,取决于内存池的实际大小)。


③ 将较大最近空闲区块回填内存池
再次,对于内存池的余量无法满足该次分配请求的情况下,则称内存池中的余量为碎片,由于每次分配的内存字节数都是 16 的倍数,则内存池余量大小的碎片一定可以归入 free_list 当中的某个节点之下。归入后再进行内存申请操作。

最后,若是内存池无法分配满足请求大小的内存区块,同时计算机内存资源也无法再分配给更多的内存。该情况下,先将碎片内存分给相应链表节点,然后 alloc 会从其所管理的内存区块中选择区块大小最接近的较大区块取出一个回填给内存池,再从中切出所需大小的内存块给客户。

可若是没有大小大于所需内存块大小的空闲管理区块,则无法完成申请操作,则抛出异常(throw exception)或终止(abort())程序(由operator::new()执行);


更具化和形象的理解见下图:



3. GNU2.9 std::alloc 源码剖析


3.1 一级分配器 / malloc_alloc

std::alloc 的实现主要有一级分配器(minor次要分配器)和二级分配器组成。一级分配器的malloc主要是采用malloc的方式进行内存空间申请和分配,使用了类似于set_new_handler()的方式,所以在GNU4.9中就已经不再是这般了。一级分配器实现如下:

#define __THROW_BAD_ALLOC  cerr << "out of memory" << endl; exit(1)
//----------------------------------------------
// 第1級配置器, minor次级配置器
//----------------------------------------------
template <int inst>
class __malloc_alloc_template {
private:
  static void* oom_malloc(size_t);
  static void* oom_realloc(void *, size_t);
  static void (*__malloc_alloc_oom_handler)();

public:
  static void* allocate(size_t n)
  {
    void *result = malloc(n);   //直接使用 malloc()
    if (0 == result) result = oom_malloc(n);
    return result;
  }
  static void deallocate(void *p, size_t /* n */)
  {
    free(p);                    //直接使用 free()
  }
  static void* reallocate(void *p, size_t /* old_sz */, size_t new_sz)
  {
    void * result = realloc(p, new_sz); //直接使用 realloc()
    if (0 == result) result = oom_realloc(p, new_sz);
    return result;
  }
  static void (*set_malloc_handler(void (*f)()))()
  { //类似 C++ 的 set_new_handler().
    void (*old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = f;
    return(old);
  }
};
//----------------------------------------------
template <int inst>
void (*__malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;

template <int inst>
void* __malloc_alloc_template<inst>::oom_malloc(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 = malloc(n);        //再次嘗試配置記憶體
    if (result) return(result);
  }
}

template <int inst>
void * __malloc_alloc_template<inst>::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);
  }
}
//----------------------------------------------
typedef __malloc_alloc_template<0>  malloc_alloc;

template<class T, class Alloc>
class simple_alloc {
public:
  static T* allocate(size_t n)
    { return 0 == n? 0 : (T*)Alloc::allocate(n*sizeof(T)); }
  static T* allocate(void)
    { return (T*)Alloc::allocate(sizeof(T)); }
  static void deallocate(T* p, size_t n)
    { if (0 != n) Alloc::deallocate(p, n*sizeof(T)); }
  static void deallocate(T *p)
    { Alloc::deallocate(p, sizeof(T)); }
};

3.2 二级分配器 / std::alloc

相较于一级分配器,二级分配器才是主要的内存空间申请和分配的方式,也即是 std::alloc 的具体实现。其主要由 allocate() 、deallocate() 、refill() 和 chunk_alloc() 四个函数来实现相关功能,同时还有 start_free、end_free两个指针表示内存池的边界,obj 联合体表示具体的已分配内存的对象, heap_size 记录 std::alloc 所管理的内存空间大小。注意到这些成员变量和成员函数均为静态成员。


3.2.1 allocate() 和 deallocate() 实现

std::alloc 的主体实现部分如下:

//第二級配置器
//----------------------------------------------
enum {__ALIGN = 8};                        //小區塊的上調邊界,这样使得所分配区块大小都是8的倍数
enum {__MAX_BYTES = 128};                  //小區塊的上限,free_list长度为16,所以16*8 = 128
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free-lists 個數, 128 / 8 = 16

//本例中兩個 template 參數完全沒有派上用場
template <bool threads, int inst> //threads模板参数与多线程运行环境相关
class __default_alloc_template {
private:
  //實際上應使用 static const int x = N
  //取代 enum { x = N }, 但目前支援該性質的編譯器不多
  static size_t ROUND_UP(size_t bytes) { //上调补齐为8的倍数
    return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
  }

private:
  union obj { //这里也可使用struct,因为其中只有一个联合成员
    union obj* free_list_link;
  };

private:
  //volatile与多线程过程中出现的指令重排禁止相关,详见:Singleton 单例模式,		 
  //https://blog.csdn.net/yueguangmuyu/article/details/115448833
  static obj* volatile free_list[__NFREELISTS];
  static size_t FREELIST_INDEX(size_t bytes) { //计算free_list节点的内存地址
    return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

  // Returns an object of size n, and optionally adds to size n free list.
  //即进行大内存划分小内存,n表示每个小内存块的大小,默认分为20个(nobjs=20),同时在其中
  //调用了chunk_alloc(),其中nobjs是引用传递,所以最终所分得的小内存块个数是自适应的。
  static void *refill(size_t n); 

  // Allocates a chunk for nobjs of size "size".  nobjs may be reduced
  // if it is inconvenient to allocate the requested number.
  //chunk表示一大块内存,申请得到大块内存之后,交由refill进行分块并将其中一块返给客户
  //控制着实际分得的小内存区块数目。
  static char *chunk_alloc(size_t size, int &nobjs);

  // Chunk allocation state.
  static char*  start_free; //内存池(memory pool)的起点
  static char*  end_free;   //内存池的终点
  static size_t heap_size;  //std::alloc所管理的内存空间的大小,即堆大小

public:
  static void * allocate(size_t n)  //n must be > 0
  {
	//一级指针指向free_list节点,二级指针指向具体的空闲内存地址
    obj* volatile *my_free_list;    //obj** my_free_list;指针的指针,二级指针
    obj* result;

    if (n > (size_t)__MAX_BYTES) { //需要分配的大小大于128bytes,则使用malloc进行内存分配
        return(malloc_alloc::allocate(n));
    }

    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;
    if (result == 0) { //如果尚未有对应n大小的空闲内存区块,则进行申请分配
        void* r = refill(ROUND_UP(n)); //这里说明传给refill()函数的n都是8的倍数
        return r; //返回申请所得的内存区块中的第一个,同时将相应free_list节点指向第二个内存区块
    }
    *my_free_list = result->free_list_link;
    return (result);
  }

  static void deallocate(void *p, size_t n)  //p may not be 0
  //没有检查*p是否是由alloc所分配出去的
  //若是没有检查,仍可并入alloc当中去,如果p所指大小不是8倍数,会造成灾难
  {
    obj* q = (obj*)p;
    obj* volatile *my_free_list;   //obj** my_free_list;
    if (n > (size_t) __MAX_BYTES) { //若是释放的内存空间大于128bytes,则使用deallocate
        malloc_alloc::deallocate(p, n);
        return;
		//没有free(),由于每个free_list节点的值并不是一成不变的,所以若是使用free很有麻烦
		//这是std::alloc的设计结构所造成的问题,所以std::alloc并没有内存归还机制
    }
    my_free_list = free_list + FREELIST_INDEX(n);
    //将 q 加入 my_free_list 链表的头部
    q->free_list_link = *my_free_list;
    *my_free_list = q;
  }

  static void * reallocate(void *p, size_t old_sz, size_t new_sz); //实现省略...
};

同时需要注意的是,之前提到过GNU2.9 容器使用的不是 std::allocator 而是 std::alloc,这样的话可以大大减少调用new的次数。在下面的代码当中可以看到端倪,其是将 __default_alloc_template<false,0> 命名别名为 alloc。

//----------------------------------------------
//对__default_alloc_template<threads, inst>类的内部变量进行定义
//类型外的对类型的静态成员类型进行数据定义
template <bool threads, int inst>
char *__default_alloc_template<threads,inst>::start_free = 0;

template <bool threads, int inst>
char *__default_alloc_template<threads,inst>::end_free = 0;

template <bool threads, int inst>
size_t __default_alloc_template<threads,inst>::heap_size = 0;

template <bool threads, int inst>
typename __default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS]
     = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
//----------------------------------------------
//令第2級配置器的名稱為 alloc
typedef __default_alloc_template<false,0> alloc;

3.2.2 chunk_alloc() 实现

函数 chunk_alloc(size_t size, int& nobjs) 是 std::alloc 当中实现内存分配的最核心的函数,其中实现了对应GNU2.9 std::alloc 运行模式中的三种获取内存的方式。chunk表示一大块内存,申请得到大块内存之后,交由refill进行分块并将其中一块返给客户,同时 chunk_alloc() 控制着实际所分得的小内存区块数目。

//----------------------------------------------
// We allocate memory in large chunks in order to
// avoid fragmentingthe malloc heap too much.
// We assume that size is properly aligned.
// We hold the allocation lock.
//----------------------------------------------
template <bool threads, int inst>
char*
__default_alloc_template<threads, inst>::
chunk_alloc(size_t size, int& nobjs)
{
  char* result;
  size_t total_bytes = size * nobjs;         //分配20块空间所需内存数
  size_t bytes_left = end_free - start_free; //内存池剩余量
//------------------------------------------------------------------------------------------
  //以下代码块对应 ② 使用内存池当中的余量
  if (bytes_left >= total_bytes) { //pool空间足以满足20块的需求
      result = start_free;         //分配20块空间给客户
      start_free += total_bytes;   //调低内存池的剩余量,start_free指针移动
      return(result);
  } else if (bytes_left >= size) { //pool空间满足一块(含)以上需求
      nobjs = bytes_left / size;   //改变需求数nobjs(注意这里nobjs是pass-by-reference)
      total_bytes = size * nobjs;  //改变需求总量
      result = start_free;
      start_free += total_bytes;   //调整start_free指针,降低内存池余量
      return(result);
//------------------------------------------------------------------------------------------
	//若是内存池中无法再分配出内存,对应 ① 向计算机申请额外的内存空间 或 ③进行空闲内存块回填
  } else {
      size_t bytes_to_get =
                 2 * total_bytes + ROUND_UP(heap_size >> 4);
      // Try to make use of the left-over piece. 尝试将内存做充分利用
	  //碎片处理
      if (bytes_left > 0) {
          obj* volatile *my_free_list =
                 free_list + FREELIST_INDEX(bytes_left);

		//将当前内存余量的下一节点指向free_list中对应节点
          ((obj*)start_free)->free_list_link = *my_free_list; 
		//将free_list中对应节点所存储指针赋值为start_free,注意my_free_list是二级指针
		//即将free_list中的对应节点中的指针指向start_free的地址
          *my_free_list = (obj*)start_free;
      }
	  
	  //申请新的内存块(chunk)
      start_free = (char*)malloc(bytes_to_get);
	  //-------------------------------------------------------------------------------------------
	  //若是申请内存操作失败
	  //以下代码对应③ 将较大最近空闲区块回填内存池
      if (0 == start_free) {
          int i;
          obj* volatile *my_free_list, *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 <= __MAX_BYTES; i += __ALIGN) { 
		  //从size开始对free_list进行遍历,寻找符合且有空闲区块的链表节点
              my_free_list = free_list + FREELIST_INDEX(i);
              p = *my_free_list;
              if (0 != p) { //该free_list内有可用区块,以下释放一块并回填给内存池
                  *my_free_list = p -> free_list_link;
				  //以下两行为将free-list内的目前第一块当作内存池(memory pool)
                  start_free = (char*)p;
                  end_free = start_free + i;
                  return(chunk_alloc(size, nobjs));//尝试再进行一次内存分配
				  //此时的pool一定能够供应至少一个区块
				  //而任何残余零头最终将被当作碎片归置入对应的free_list节点下
                  //Any leftover piece will eventually make it to the
                  //right free list.
              }
          }
		  //--------------------------------------------------------------------------------------
          end_free = 0; //In case of exception.没有符合的区块可供回填再分配,即无法满足内存分配操作
		  //将end_free指针调整为0,表示无法满足操作
          start_free = (char*)malloc_alloc::allocate(bytes_to_get);
		  //使用一级配置器,即使用malloc或设置定制的set_malloc_handler(实际是oom-handler)
		  //可能会导致抛出异常或是内存不足的情况得以改善
          //This should either throw an exception or
          //remedy the situation. Thus we assume it
          //succeeded.
      }
	  //-------------------------------------------------------------------------------------------
	  //若是申请内存操作成功
	  //以下代码对应① 向计算机申请额外的内存空间
      heap_size += bytes_to_get; //调整总的分配内存总量
      end_free = start_free + bytes_to_get; //将end_free指针进行调整
      return(chunk_alloc(size, nobjs)); //得到新的内存后再试一次
  }
}

3.2.3 refill() 实现

refill(size_t n) 函数用来对内存区块进行划分,期间调用 chunk_alloc() 函数来实现对内存空间的申请/取用(从内存池中),并获取所获内存空间所能容纳的 obj 个数:nobjs,并依据 nobjs 的数值再对所获取的内存空间进行区块分割。

//----------------------------------------------
// Returns an object of size n, and optionally adds
// to size n free list.We assume that n is properly aligned.
// We hold the allocation lock.
//----------------------------------------------
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::
refill(size_t n) //n在allocate()函数当中已被调整为8的倍数
{
    int nobjs = 20; //预设取20个区块
	//nobjs是pass-by-reference的,可进行更改
	//先拿到很多,再进行分配
    char* chunk = chunk_alloc(n,nobjs); 
    obj* volatile *my_free_list;   //obj** my_free_list;
    obj* result;
    obj* current_obj;
    obj* next_obj;
    int i;
	
	//如果只拿到了一个,直接return该地址
    if (1 == nobjs) return(chunk);
	
	//以下开始将所得区块挂上free_list
    my_free_list = free_list + FREELIST_INDEX(n);
    //Build free list in chunk
    result = (obj*)chunk;
	//所谓切割就是将指针所指处转型为obj类型,然后取其obj->free_list_link,循环往复
	//这里+n的原因为第0个即result的地址要返给客户,
	//实际从第2块开始加入free_list + FREELIST_INDEX(n);
    *my_free_list = next_obj = (obj*)(chunk + n);
    for (i=1;  ; ++i) { //总共进行nobjs-1次,在chunk_alloc中分配了nobjs块内存,其中一块返给客户
      current_obj = next_obj;
      next_obj = (obj*)((char*)next_obj + n);
      if (nobjs-1 == i) { //最后一次进行分割,将最后一块区块的free_list_link赋值为空
          current_obj->free_list_link = 0;
          break;
      } else {
          current_obj->free_list_link = next_obj;
      }
    }
    return(result); //返回第一个区块的地址
}

3.3 std::alloc 如何提高内存利用率

这里的内存利用率是指存储的实际数据所占整个占用的内存空间的比例,相对的,若是内存利用率高,则控制信息所占的空间比例就要低(cookie的数目很少,甚至没有)。

std::alloc 就可以做到这点,其具体的方式如下图所示:



4. GNU2.9 std::alloc 技法总结

  • 对于源码中的 obj* volatile *my_free_list; 和 obj* volatile *my_free_list, *p; 极易造成混淆,造成指针级别难以分辨,去掉关键字修饰,为obj* *p1, *p2;,可看出是关于obj类型的二级指针和一级指针,所以写为obj **p1, *p2;或obj** p1; obj* p2;代码的可读性要好很多;
  • 对于源码中的if(0==start_free)等这种常量作为左值的判断情形,这么做的缘由是,在进行编码时,有时可能会把==误打为=,此时编译器将其作为赋值操作且不会报错,但是在debug过程当中有时极难发现,特别是较长的代码情形下,所以采用该形式,利用常量不能够做左值的性质,杜绝将==误打为=的错误情形,因为此时若是误打,编译器会报错;
  • 在进行变量定义时,特别是指针变量进行定义时,最好在要使用时才进行定义,并最好进行有意义的初始化。这里的 chunk_alloc() 函数中的 size_t bytes_to_get 变量声明 就违反了这一准则,该准则实际就是《Effective C++》条款26. 延后并以具明显意义初值定义变量;
  • 尽量保持代码的高可读性,而不是单纯喜欢炫技。在一级配置器实现中 static void(*set_malloc_handler(void (*f)()))()可以使用 typedef 命名别名使得可读性更好,如typedef void(*H)(); static H set_malloc_handler(H f);,类似于:
    在这里插入图片描述
  • 此外,由于 std::alloc 本身的设计缺陷,导致 deallocate() 函数完全没有调用 free() 和 delete(),具体原因见3.2.1当中代码注释;
  • 可将 std::alloc 由 C++ 转换为纯 C 的版本,移植方式如下:
    在这里插入图片描述
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值