C++ STL : SGI-STL空间配置器源码剖析


空间配置器的概念

空间配置器,顾名思义就是为各个容器高效的管理空间(空间的申请与回收) 的,在默默地工作。

在之前所有模拟实现的容器中,对于空间的管理都是通过直接调用new和delete来进行的,虽然代码可以正常运行,但是还是存在着大量的缺点。

  • 空间申请与释放需要用户自己管理,容易造成内存泄漏
  • 频繁向系统申请小块内存块,容易造成内存碎片
  • 频繁向系统申请小块内存,影响程序运行效率
  • 直接使用malloc与new进行申请,每块空间前有额外空间浪费
  • 申请空间失败怎么应对
  • 代码结构比较混乱,代码复用率不高
  • 未考虑线程安全问题

基于上述的缺点,需要设计一个高效的内存管理机制。


SGI-STL空间配置器

上面所说的缺点中,最主要的其实就是频繁向系统申请小块内存块而导致的内存碎片问题。因此在SGI-STL中以128字节为分界线,超过128字节的视为大块内存,将其交给一级空间配置器处理。小于等于128字节的视为小块内存,将其交付给二级空间配置器处理。
在这里插入图片描述
为什么内核中已经有了一个类似的slab分配器来管理小块内存,STL还需要自己弄一个空间配置器呢?

  1. 内核是针对操作系统中所有程序的,他申请和释放内存的消耗会非常大
  2. STL需要的全部都是小块内存,并且需求比较集中,如果自己设计一个效率更高,还能顺带解决内存碎片的问题

一级空间配置器

一级空间配置器主要就是对malloc和free进行了一层封装,同时加上了对oom(内存不足)的处理方法

每当通过malloc或者realloc申请空间时,如果出现了内存不足的情况,就会调用oom来进行处理。

//直接对malloc进行封装
static void * allocate(size_t n)
{
    void *result = malloc(n);
    //申请失败时,调用设置的回调函数来进行处理
    if (0 == result) result = oom_malloc(n);
    return result;
}

//free
static void deallocate(void *p, size_t /* n */)
{
    free(p);
}

//realloc
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
    void * result = realloc(p, new_sz);
    if (0 == result) result = 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);
}

在oom的处理方法中可以看到,其通过一个死循环,不断地去尝试申请空间,他期待能够在某次调用后能够获取足够的空间并将其返回,但是如果用户没有设置处理内存不足的回调函数,他就会通过抛出一个BAD_ALLOC异常来强行中止程序。

template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endif

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;
        //如果没有设置处理方法,则直接抛出一个BAD_ALLOC的异常
        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);
    }
}

从上面就可以看出来,一级空间配置器主要为大块空间进行管理,所以其操作并没有那么细致,只是简单的为C语言的内存管理函数malloc、realloc、free进行简单的封装。同时,如果出现了内存不足的情况,就会在一个死循环中不断使用用户提供的回调函数来进行问题的处理,期待能够获取足够的空间来完成任务。但是如果用户并没有提供这个函数,则会直接抛出BAD_ALLOC异常来终止程序,所以设计一个内存不足的处理程序是用户的责任


二级空间配置器

二级空间配置器专门负责处理小于128字节的小块内存。采用了内存池的技术来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度与高效管理。

首先申请一大块内存,并且用类似哈希桶的结构来维护这个内存池,每一个桶中装载一个free-list单链表。总共维护16个free-lists,各自管理大小以8字节为倍数,依次是8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128字节。

例如:
在这里插入图片描述

template <bool threads, int inst>
class __default_alloc_template {

private:
  // Really we should use static const int x = N
  // instead of enum { x = N }, but few compilers accept the former.
# ifndef __SUNPRO_CC
    enum {__ALIGN = 8};//倍数
    enum {__MAX_BYTES = 128};//最大空间
    enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//链表个数
# endif
  //将申请的空间上调至8的倍数
  static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
  }
__PRIVATE:
  //free-list的节点构造
  union obj {
        union obj * free_list_link;
        char client_data[1];    /* The client sees this.        */
  };
private:
# ifdef __SUNPRO_CC
    static obj * __VOLATILE free_list[]; 
        // Specifying a size results in duplicate def for 4.1
# else
    static obj * __VOLATILE free_list[__NFREELISTS]; 
# endif
  //根据申请的空间大小,决定使用几号free-list
  static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

申请空间

申请空间的方式很简单,首先根据你要申请的空间大小,来找到对应的free-list的位置,如果没有达到8的倍数,则会将其上调,例如申请5个字节,则会补充到8个字节,然后找到0号链表。
在这里插入图片描述

static void * allocate(size_t n) {
 obj * __VOLATILE * my_free_list;
 obj * __RESTRICT result;
 // 检测用户所需空间释放超过128(即是否为小块内存)
if (n > (size_t) __MAX_BYTES)
{
// 不是小块内存交由一级空间配置器处理
return (malloc_alloc::allocate(n));
}
// 根据用户所需字节找到对应的桶号
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
// 如果该桶中没有内存块时,向该桶中补充空间
if (result == 0)
{
// 将n向上对齐到8的整数被,保证向桶中补充内存块时,内存块一定是8的整数倍
void *r = refill(ROUND_UP(n));
return r;
}
// 维护桶中剩余内存块的链式关系
*my_free_list = result -> free_list_link;
return (result);
};

补充内存块

如果对应位置的链表中有内存对象,则直接取走,如果没有,则会从内存池中拿出20个对应大小的内存对象,取走一个后将剩下的19个挂载到链表上。 如果内存池空间剩余空间大于1个,小于19个,则会先将一个给使用者,剩下的全部挂载到链表上,之后会通过chunk_alloc来重新配置堆空间,来补充内存池。

在这里插入图片描述

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
    int nobjs = 20;//默认获取对象个数
    char * chunk = chunk_alloc(n, nobjs);//获取nobjs个内存对象
    obj * __VOLATILE * my_free_list;
    obj * result;
    obj * current_obj, * next_obj;
    int i;

    if (1 == nobjs) return(chunk);//如果剩余空间只够一个,则直接拿给使用者
    my_free_list = free_list + FREELIST_INDEX(n);

    /* Build free list in chunk */
      result = (obj *)chunk;//返回一个给使用者
      *my_free_list = next_obj = (obj *)(chunk + n);//将内存对象插入链表中
      //从第一个开始,因为第0个已经返回给使用者
      for (i = 1; ; i++) {
        current_obj = next_obj;
        next_obj = (obj *)((char *)next_obj + n);
        if (nobjs - 1 == i) {
            current_obj -> free_list_link = 0;
            break;
        } else {
            current_obj -> free_list_link = next_obj;
        }
      }
    return(result);
}

从内存池中索要空间

在这里插入图片描述

template <int inst>
char* __default_alloc_template<inst>::chunk_alloc(size_t size, int& nobjs)
{
	// 计算nobjs个size字节内存块的总大小以及内存池中剩余空间总大小
	char * result;
	size_t total_bytes = size * nobjs;
	size_t bytes_left = end_free - start_free;
	
	// 如果内存池可以提供total_bytes字节,返回
	if (bytes_left >= total_bytes)
	{
		result = start_free;
		start_free += total_bytes;
		return(result);
	}
	else if (bytes_left >= size)
	{
		// nobjs块无法提供,但是至少可以提供1块size字节内存块,提供后返回
		nobjs = bytes_left/size;
		total_bytes = size * nobjs;
		result = start_free;
		start_free += total_bytes;
		return(result);
	}
	else
	{
		// 内存池空间不足,连一块小块村内都不能提供
		// 向系统堆求助,往内存池中补充空间
		// 计算向内存中补充空间大小:本次空间总大小两倍 + 向系统申请总大小/16
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		
		// 如果内存池有剩余空间(该空间一定是8的整数倍),将该空间挂到对应哈希桶中
		if (bytes_left > 0)
		{
			// 找对用哈希桶,将剩余空间挂在其上
			obj ** my_free_list = free_list + FREELIST_INDEX(bytes_left);
			((obj *)start_free) -> free_list_link = *my_free_list;
			*my_ree_list = (obj *)start_free;
		}
		// 通过系统堆向内存池补充空间,如果补充成功,递归继续分配
		start_free = (char *)malloc(bytes_to_get);
		if (0 == start_free)
		{
			// 通过系统堆补充空间失败,在哈希桶中找是否有没有使用的较大的内存块
			int i;
			obj ** my_free_list, *p;
			
			for (i = size; i <= __MAX_BYTES; i += __ALIGN)
			{
				my_free_list = free_list + FREELIST_INDEX(i);
				p = *my_free_list;
				
				// 如果有,将该内存块补充进内存池,递归继续分配
				if (0 != p)
				{
					*my_free_list = p -> free_list_link;
					start_free = (char *)p;
					end_free = start_free + i;
					return(chunk_alloc(size, nobjs));
				}
			}
			// 山穷水尽,只能向一级空间配置器求助
			// 注意:此处一定要将end_free置空,因为一级空间配置器一旦抛异常就会出问题
			end_free = 0;
			start_free = (char *)malloc_alloc::allocate(bytes_to_get);
		}
		// 通过系统堆向内存池补充空间成功,更新信息并继续分配
		heap_size += bytes_to_get;
		end_free = start_free + bytes_to_get;
		return(chunk_alloc(size, nobjs));
	}
}

空间回收

在这里插入图片描述

static void deallocate(void *p, size_t n)
{
	obj *q = (obj *)p;
	obj ** my_free_list;
	
	// 如果空间不是小块内存,交给一级空间配置器回收
	if (n > (size_t) __MAX_BYTES)
	{
		malloc_alloc::deallocate(p, n);
		return;
	}
	
	// 找到对应的哈希桶,将内存挂在哈希桶中
	my_free_list = free_list + FREELIST_INDEX(n);
	q -> free_list_link = *my_free_list;
	*my_free_list = q;
}

对于二级空间配置器,其主要管理小块内存。通过申请一个空间作为内存池,借助哈希桶来进行内存分配与回收管理,其中以8位倍数,维护了从8到128字节的16个链表。当申请的内存小于等于128时,就会到哈希桶对应的链表中下取出内存对象。如果链表空间不够,则会从池中取出20个内存对象,1个拿走另外19个继续挂载到对应的表下。如果要归还空间时,找到对应的链表后再将其挂载进去即可。


内存碎片

外碎片

外碎片问题其实就是最为常见的内存碎片问题。
例如我们有16字节的内存,我们依次申请了4个字节,8个字节,4个字节的空间
在这里插入图片描述
之后再释放掉那两个4字节的空间,此时还剩8字节的空闲空间
在这里插入图片描述
但是如果我们想再存入8个字节的空间,此时就会出现问题,因为我们申请释放了太多的小空间,导致此时剩余空间不连续,虽然空间足够,但是无法给大块数据进行存储。外碎片的本质其实就是大量小块空间的申请

内碎片

SGI-STL空间配置器的核心就是为了解决外碎片的问题,所以使用了二级空间配置器来对小块空间进行管理。但是在管理的时候,又带来了相对来使影响较小的内碎片问题。

从前面的描述也可以看到,如果申请的小块内存大小不到8的倍数,会将其进行提升,也就是说如果我们申请的是5字节,则分配的其实是8字节。但是这里的3字节用不上,这种多余的内存也就是内碎片。所以可以看出,SGI-STL只是将外碎片问题转换成了相对来说影响较小的内碎片问题,本质上还是存在内存碎片。


空间配置器的再次封装

为了方便用户的使用SGI-STL对空间配置器进行了进一步的封装,让用户可以直接根据类型来完成空间的申请

空间配置由宏__USE_MALLOC控制,默认使用二级空间配置器

#ifdef __USE_MALLOC
typedef malloc_alloc alloc;
typedef malloc_alloc single_client_alloc;
#else
 
 // 二级空间配置器定义
#endif
// 注意:该类只负责申请与归还对象的空间,不否则空间中对象的构造与析构
template<class T, class Alloc>
class simple_alloc
{
public:
	 // 申请n个T类型对象大小的空间
	 static T *allocate(size_t n)
	 { 
	 	return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); 
	 }
	 
	 // 申请一个T类型对象大小的空间
	 static T *allocate(void)
	 { 
	 	return (T*) Alloc::allocate(sizeof (T));
	 }
	 
	 // 释放n个T类型对象大小的空间
	 static void deallocate(T *p, size_t n)
	 { 
		 if (0 != n) 
		 	Alloc::deallocate(p, n * sizeof (T));
	 }
	 
	 // 释放一个T类型对象大小的空间
	 static void deallocate(T *p)
	 { 
	 	Alloc::deallocate(p, sizeof (T)); 
	 }
};

并且考虑到不是所有类型都需要调用构造和析构函数,所以他将空间管理和构造析构给拆分开来,通过placement-new来进行构造。

// 归还空间时,先先调用该函数将对象中资源清理掉
template <class T>
inline void destroy(T* pointer) {
 	pointer->~T();
}

// 空间申请好后调用该函数:利用placement-new完成对象的构造
template <class T1, class T2>
inline void construct(T1* p, const T2& value) 
{
 	new (p) T1(value);
}
  1. 在释放对象时,需要根据对象的类型确定是否调用析构函数(类型萃取)
  2. 对象的类型可以通过迭代器获萃取到

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/107786452