STL空间配置器(一)
来源:互联网 发布:淘宝详情页编辑 编辑:程序博客网 时间:2024/06/05 17:14
STL空间适配器(一)
Author:胡建
Time:2016/4/5
这是STL学习的第一部分,空间适配器,所谓空间适配器,就是用来管理内存的一个器具。对于STL来说,空间适配器是它可以正常工作的基础,也为它可以高效工作提供了动力。对于使用STL来说,它是不和用户直接打交道的,而是隐藏在一切STL组建之后,默默为各种内存申请提供支持的。
对于c++用户来说,new和delete很熟悉,这两个函数可以分别完成内存的申请和释放,和c里面的malloc和free如出一辙,SGI 有一个标准空间适配器,同时还有一个特殊空间适配器,标准空间适配器为:std::allocator,者个适配器只是对new和delete的浅层包装,所以没有什么技术含量,所以在SGI中从没使用过这个标准适配器。另一个空间适配器是std:alloc,这是一个具有次分配能力的特殊空间适配器,它具有一级和二级适配器,它们协调工作。
Std::alloc的主要思想是:定义一个空间大小阈值,128bytes,如果申请的空间大于128bytes,那么就调用第一级空间适配器来完成分配工作,如果小于128bytes,那么久调用第二级空间适配器来完成。对于第一级适配器,直接调用malloc和free来完成分配与释放内存的工作(没有调用new和delete),最为重要的是,第一级适配器具有new-handle机制,用户可以指定当出现out-of-memory时的处理函数,在SGI里面,当第一级alloc失败时,会接着调用oom_alloc函数来尝试分配内存,如果oom发现没有指定new-handler函数的话,那就无能为力了!会抛出__THROW_BAD_ALLOC这个异常,下面是第一级适配器的主要流程(对于alloc来讲,其他如realloc一样):
1、空间分配器的“分线器”
static void * allocate(size_t n){ void *result = malloc(n);//直接使用第一级分配器,直接使用malloc if (0 == result) result = oom_malloc(n);//第一级分配器失效了,那就使 用第二级分配器,oom(out of memeory) return result;//将分配的空间以void*的方式返回,用户可以随意转化为需要的类型}
n为我们想要申请的内存,如果malloc可以满足要求的话,直接返回,否则交由oom_malloc()来处理。
2、具有new-handler的oom_malloc
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);//如果内存已经得到满足的话,那么就可以返回了,如果不满足,那么 //就继续释放,分配.... }}
其中的__malloc_alloc_oom_handler就是由用户设定的new-handler,我们可以通过下面的函数来设定这个句柄:
static void (* set_malloc_handler(void (*f)()))(){ void (* old)() = __malloc_alloc_oom_handler;//old handler of outing of memory __malloc_alloc_oom_handler = f;//new handler for hander the out of memory return(old);}
是不是看起来oom_malloc也没做什么事情,就是一直在循环申请内存?在一个循环里oom_handler->malloc….就等着某一个时刻成功申请到内存了,就可以返回交差了!!
我们重点来看看第二级配置器,这才是SGI的经典(个人),我们需要再次知道第二级分配器是怎么“被”工作的,当用户申请的内存大小小于128bytes时,SGI配置器“分线器”就会将这个工作交由第二级分配器来完成。此时,第二级分配器就要开始工作了。第二级分配器的原理较为简单,就是向内存池中申请一大块内存空间,然后按照大小分为16组,(8,16…..128),每一个大小都对应于一个free_list链表,这个链表上面的节点就是可以使用的内存空间,需要注意的是,配置器只能分配8的倍数的内存,如果用户申请的内存大小不足8的倍数,配置器将自作主张的为用户上调到8的倍数,所以有时候你明明超出边界了但是系统却没有阻止你的行为的时候,你应该知道是SGI空间配置器救了你。
当然,第二级配置器的原理远没有这么简单,上面我们说到第二级配置器如何管理内存,现在,我们要开始为用户分配内存和回收内存了。
当用户申请一个内存后,第二级配置器首先将这个空间大小上调到8的倍数,然后找到对应的free_list链表,如果链表尚有空闲节点,那么就直接取下一个节点分配给用户,如果对应链表为空,那么就需要重新申请这个链表的节点,默认为20个此大小的节点。如果内存池已经不足以支付20个此大小的节点,但是足以支付一个或者更多的该节点大小的内存时,返回可完成的节点个数。如果已经没有办法满足该大小的一个节点时,就需要重新申请内存池了!所申请的内存池大小为:2*total_bytes+ROUND_UP(heap_size>>4),total_bytes是所申请的内存大小,SGI将申请2倍还要多的内存。为了避免内存碎片问题,需要将原来内存池中剩余的内存分配给free_list链表。如果内存池申请内存失败了,也就是heap_size不足以支付要求时,SGI的次级配置器将使用最后的绝招–>查看free_list数组,查看是否有足够大的没有被使用的内存,如果这些办法都没有办法满足要求时,只能调用第一级配置器了,我们需要注意,第一级配置器虽然是用malloc来分配内存,但是有new-handler机制(out-of-memory),如果无法成功,只能抛出bad_alloc异常而结束分配。
上面说到的配置器“分线器”,其实就是次级配置器的入口,如果次级配置器发现所需要分配的内存大于128bytes时,就会交由第一级配置器去完成,否则由自己完成。
下面,我们就来看看这个次级配置器是怎么实现的:
enum {__ALIGN = 8};//小型区域的上调边界,用户如果申请30bytes大小内存,系统将返回32bytes给用户 enum {__MAX_BYTES = 128};//小型区域的上限,超过这个大小将直接由第一级配置器直接配置内存 enum {__NFREELISTS = __MAX_BYTES/__ALIGN};//free-list链表的个数,从8一直到128,总共需要16个链表维护每个级别大小的链表
我相信不需要说什么就可以明白,上面的代码是在定义最小河最大的空间大小,和需要的链表的个数,为什么要定义这个呢?
也就是说,如果你觉得SGI STL的配置器过于频繁的调用次级配置器,那么你可以修改进入次级配置器的条件,比如可以修改成16-128,或者32-256等等。我们需要明白的事情是,虽然次级配置器解决了内存碎片的问题,但是给内存管理带来了额外的负担,有可能需要不断去调整free_list,甚至去反过来调用第一级配置器,所以如果你觉得进入配置器的区间不够合理的话,可以自己调整!(但别玩死了!)。
static size_t ROUND_UP(size_t bytes) { return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); }
上面这个函数返回离bytes最近的可以被8整除的整数,取上。
下面我们看看free_list的节点的数据结构:
union obj { union obj * free_list_link; char client_data[1]; /* The client sees this. */ };
如此巧妙地运用union来管理节点,如果还没有被分配,那么free_list_link有效,如果已经分配给用户,那么client_data[1]有效。不会造成多余的浪费!
下面这个定义就是我们所说的free_list链表数组,每一个数组元素都将对应一个链表:
static obj * __VOLATILE free_list[];
下面这个函数将找到所申请的内存所对应的空闲链表,注意,SGI将用户的内存申请大小上调至8的倍数:
static size_t FREELIST_INDEX(size_t bytes) { return (((bytes) + __ALIGN-1)/__ALIGN - 1); }
次级配置器将会使用内存池,所以下面给出内存池的区间。
static char *start_free; //内存池的开始位置 static char *end_free;//内存池的结束位置
下面就是激动人心的内存分配函数了,该函数将首先判断区间大小,如果大于128bytes,就调用第一级配置器,小于128bytes就寻找对应的free_list链表,如果有可用的,直接拿,如果没有,则调用函数refill来重新分配节点(20个)。
/* n must be > 0 */ static void * allocate(size_t n) { obj * __VOLATILE * my_free_list; obj * __RESTRICT result; //如果空间大于128bytes,直接调用第一级空间配置器 if (n > (size_t) __MAX_BYTES) { return(malloc_alloc::allocate(n)); } //找到相应的free_list的链表 my_free_list = free_list + FREELIST_INDEX(n); // 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 result = *my_free_list; //如果该链表已经没有可以使用的节点了,那么久重新分配节点 if (result == 0) { //refill函数用来为该链表重新分配节点(20个) void *r = refill(ROUND_UP(n)); return r; } //修改free_list链表,拿掉一个 *my_free_list = result -> free_list_link; return (result); };
有申请就需要回收内存,下面就是次级配置器的空间释放函数,该函数将判断区间大小,大于128bytes直接调用第一级配置器来回收内存,小于128bytes则回收到相应的free_list链表中,较为简单。
/* p may not be 0 */ static void deallocate(void *p, size_t n) { obj *q = (obj *)p; obj * __VOLATILE * my_free_list; //大于128bytes,交由第一级配置器完成回收 if (n > (size_t) __MAX_BYTES) { malloc_alloc::deallocate(p, n); return; } //找到对应的链表 my_free_list = free_list + FREELIST_INDEX(n); // acquire lock# ifndef _NOTHREADS /*REFERENCED*/ lock lock_instance;# endif /* _NOTHREADS */ //将这个节点加到这个链表里面去 q -> free_list_link = *my_free_list; *my_free_list = q; // lock is released here }
下面我们来看看refill函数,这个函数完成重新给链表分配节点的工作。新的空间将从内存池中取到,(内存池由chunk_alloc管理),缺省的话只取20个新节点,但有可能小于20个节点。
template <bool threads, int inst>void* __default_alloc_template<threads, inst>::refill(size_t n){ int nobjs = 20; //缺省取20个新节点 //向内存池申请20个该链表节点 char * chunk = chunk_alloc(n, nobjs); obj * __VOLATILE * my_free_list; obj * result; obj * current_obj, * next_obj; int i; //很不幸但是又很幸运,我们取得了1个节点,那就直接返回吧,不需要更新链表了 if (1 == nobjs) return(chunk); //否则要开始准备调整free_list链表了,这也是次级配置器内存管理开销的地方 //my_free_list将指向我们的目标链表头 my_free_list = free_list + FREELIST_INDEX(n); /* Build free list in chunk */ result = (obj *)chunk;//这一块是准备返回给用户的! //将free_list指向新配置的空间里面去 *my_free_list = next_obj = (obj *)(chunk + n); //第一块已经返回给用户,从第二块开始把这块空间串起来 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);}
下面就是最为重要的也是最难的内存池了。当然,下面这个函数只是从内存池中取出空间给free_list用而已。不过整个过程很曲折,很经典,需要仔细品味,很棒的设计!
char*__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs){ char * result; size_t total_bytes = size * nobjs; //这个是内存池中剩余的空间,这是一块连续的空间 size_t bytes_left = end_free - start_free; //如果内存池中的空间足以支付申请,那么只是改变内存池的剩余大小就ok啦 if (bytes_left >= total_bytes) { result = start_free; start_free += total_bytes; return(result); } //否则,看看是不是足以支付至少一个节点的空间,如果可以,那么也不要灰心 else if (bytes_left >= size) { //计算出可以支付的节点个数 nobjs = bytes_left/size; //调整内存池大小 total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return(result); } //这是最惨的,内存池已经没有足够的内存来支付申请了,只好向heap申请空间了 //申请的空间大小是所要求的2倍还要多一点 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); ((obj *)start_free) -> free_list_link = *my_free_list; *my_free_list = (obj *)start_free; } //重新之指定内存池区间,就是要malloc!!! start_free = (char *)malloc(bytes_to_get); //如果malloc失败 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. //上面说的很明白,去free_list链表里面找 //将没有被使用的内存释放了!!! 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; //每次释放一个free_list节点,就再次调用自己尝试能否成功! //这是贪心吗?可能不是,是愚蠢!!! return(chunk_alloc(size, nobjs)); // Any leftover piece will eventually make it to the // right free list. } } //哎,已经没有办法了,向第一级配置器求助吧,好歹那里有out-of-memory机制 //可以处理malloc失败的问题,当然前提是需要有合适的new-handler end_free = 0; // In case of exception. start_free = (char *)malloc_alloc::allocate(bytes_to_get); // 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; return(chunk_alloc(size, nobjs)); }}
讲完了上面的内容,我们现在应该很清楚次级配置器是怎么工作的了,其实,我们可以看到第一级配置器就是为了配合第二级配置器而存在的,重点在于次级配置器的设计原理。最后我们来走一遍配置器的流程:
Alloc_bytes is the ask memory.If(Alloc_bytes>128) First_alloc work start...Else Second_alloc work start....In Second_alloc:If(free_list!=NULL) Get an node of this free list and return itElse Refill function start to work... Chunk_alloc function start to work..In chunk_alloc:If memory pool’s size bigger wanted,just assignElse if the memory pool’s size can reach more than 1 nodesThen alloc...Else First allocator start to work... ...call itself ....
——————–2016/4/5 胡健—————–
- STL-空间配置器(一)
- STL空间配置器(一)
- 【STL】空间配置器剖析(一)
- STL(一):allocator 空间配置器
- STL空间配置器(一)
- 【STL深入学习】SGI STL空间配置器详解(一)-第一级空间配置器
- 【STL深入学习】SGI STL空间配置器详解(一)-第一级空间配置器
- 《STL剖析》——空间配置器(一)
- STL深入探究(一、空间配置器)
- stl源码剖析(一)空间配置器
- [C++]STL-空间配置器(一)
- STL 空间配置器 allocator<一>
- SGI STL空间配置器详解(一)-第一级空间配置器
- SGI STL空间配置器详解(一)-第一级空间配置器
- STL源码:空间配置器(一)SGI的空间配置器
- 【STL】SGI空间配置器(一):一级空间配置器
- SGI STL空间配置器详解(一)-第一级空间配置器
- 【STL】STL空间配置器
- PAT乙级1008-剪刀石头布
- 移动h5自适应布局
- 水晶报表表格高度自适应设置方法
- 多态
- Android的性能模式:救援技巧
- STL空间配置器(一)
- 润乾报表使用记
- Selenium启动Chrome时,加载用户配置文件
- Shortest Proper Prefix
- Http响应码及其含义
- 如何使用 Google Web Fonts?
- JavaWeb——文件上传和下载
- 关于input标签的一些常识
- CCF 窗口