STL容器内存配置器
Posted mr_yu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STL容器内存配置器相关的知识,希望对你有一定的参考价值。
本系列文章更多是笔记形式,希望能在总结过程中将一些东西理顺。难免出错,欢迎指正。
STL六大功能组件:
1.容器(containers);2.算法(algorithm);3.迭代器(iterator);4.仿函数(functors);5.配接器(adapters);6.配置器(allcators)。
各个功能组件间存在交互关系,这里不涉及这些内容,本篇文章讨论容器的内存配置。
首先,容器用来存放数据,那么存放数据之前必须向系统申请内存资源。我们知道c++中通常用(::operator new/::operator new[])来为对象分配内存,并调用对应的构造函数构造对象。
例如: class Foo { ... }; Foo * f = new Foo; delete f;
这个过程分两步: 1. ::operator new 配置内存; 2.调用Foo::Foo() 在申请的内存上构建对象.
STL的配置器也分两个过程进行:
1.定义std::alloc::allocate()负责申请空间, std::alloc::deallocate() 负责释放空间
2.对象构造和析构分别调用 ::construct()和::destroy() --这两个函数可查阅<<c++ primer>>
实现的代码文件在结构如下:
<memory>:
1.<stl_construct.h>:定义了全局的construct()和destroy(),完成对象的构造和析构,符合STL标准规范
2.<stl_alloc.h>:定义了一,二级配置器彼此合作,名称为alloc
3.<stl_uninitialized.h>:定义一些全局函数用来填充或复制大块内存数据,这里不想谈。
但是STL的容器所使用的heap内存是由SGI特殊的空间配置器 std::alloc来完成的,说他特殊是因为它不符合SGI标准,但是SGI本身有标准的空间配置器 std::allocator,
但因为其效率相对前者较低,所以容器的空间配置器为 std::alloc
例如 vector的声明: template<class T, class Alloc = alloc>
class vector{ ... }
其中alloc便是std::alloc,默认使用这个。
刚才说到SGI的标准配置器效率不高,那么这个std::alloc效率又高在哪里呢?
答案其实就在<stl_alloc.h>中第一的一二级配置器的配合使用上.
SGI的标准的配置器其实就是对 ::operator new()和 ::operator delete()的简单的封装,而这两个函数相当于c 中的malloc()和free()函数。
而std::alloc的分配策略如下 :
1.当需要配置的区块 大于 128 bytes时,直接调用一级配置器,也就是封装 malloc()和free()
1 #if 0 2 # include<new> 3 # define __THROW_BAD_ALLOC throw bad_alloc 4 #elif !defined(__THROW_BAD_ALLOC) 5 # include<iostream.h> 6 # define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1) 7 #endif 8 9 template<int inst> 10 class __malloc_alloc_template 11 { 12 private: 13 static void * oom_malloc(size_t); //oom:out_of_memory,当malloc不成功时调用此函数 14 static void * oom_realloc(void *, size_t); //当realloc()失败时调用 15 static void (* __malloc_alloc_oom_handler)(); //当申请失败时,可以自己定制的一个处理函数,此函数类似调用::operator new时的全局std::new_handler() 16 //很重要 17 18 public: 19 static void * allocate(size_t n) 20 { 21 void * result = malloc(n); 22 if (0 == result) 23 { 24 result = oom_malloc(n); 25 } 26 27 return result; 28 } 29 30 static void * deallocate(void *p, size_t n) 31 { 32 free(p); 33 } 34 35 static void * reallocate(void *p, size_t new_sz) 36 { 37 void *result = realloc(p, new_sz); 38 if ( 0 == result) 39 { 40 result = oom_realloc(p, new_sz); 41 } 42 43 return result; 44 } 45 46 //set __oom_handler 47 static void (* set_malloc_handler(void (*f)())) () //由于没有用::operator new来配置内存,所以不能调用c++机制的 new_handler(下篇文章详谈),只能自己定制 48 { 49 void (* old)() = __malloc_alloc_oom_handler; //一般思路就是,设置新的,返回旧的 50 __malloc_alloc_oom_handler = f; 51 52 return old; 53 } 54 }; 55 56 // init static func handler 57 template <int inst> 58 void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0; 59 60 template <int inst> 61 static void * __malloc_alloc_template<inst>::oom_malloc(size_t n) 62 { 63 void (* my_malloc_handler)(); 64 void * result; 65 66 for (;;) 67 { 68 my_malloc_handler = __malloc_alloc_oom_handler; 69 if (0 == my_malloc_handler) 70 { 71 __THROW_BAD_ALLOC; 72 } 73 (*my_malloc_handler)(); //若是有申请失败处理函数,则调用之,因为按照c++的规矩,这个函数一般要进行收集一些能用的内存,供malloc下次调用,或者直接退出程序 74 result = malloc(n); 75 76 if (result) 77 { 78 return result; 79 } 80 } 81 } 82 83 template <int inst> 84 static void * __malloc_alloc_template::oom_realloc(void *p, size_t new_sz) 85 { 86 void (* my_realloc_handler)(); 87 void result; 88 89 for (;;) 90 { 91 my_realloc_handler = __malloc_alloc_oom_handler; 92 if (0 == my_realloc_handler) 93 { 94 __THROW_BAD_ALLOC; 95 } 96 97 (*my_realloc_handler)(); 98 99 result = realloc(p, new_sz); 100 if(result) 101 { 102 return result; 103 } 104 } 105 } 106 107 typedef __malloc_alloc_template<0> malloc_alloc;
1 #if 0 2 # include<new> 3 # define __THROW_BAD_ALLOC throw bad_alloc 4 #elif !defined(__THROW_BAD_ALLOC) 5 # include<iostream.h> 6 # define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1) 7 #endif 8 9 template<int inst> 10 class __malloc_alloc_template 11 { 12 private: 13 static void * oom_malloc(size_t); 14 static void * oom_realloc(void *, size_t); 15 static void (* __malloc_alloc_oom_handler)(); 16 17 public: 18 static void * allocate(size_t n) 19 { 20 void * result = malloc(n); 21 if (0 == result) 22 { 23 result = oom_malloc(n); 24 } 25 26 return result; 27 } 28 29 static void * deallocate(void *p, size_t n) 30 { 31 free(p); 32 } 33 34 static void * reallocate(void *p, size_t new_sz) 35 { 36 void *result = realloc(p, new_sz); 37 if ( 0 == result) 38 { 39 result = oom_realloc(p, new_sz); 40 } 41 42 return result; 43 } 44 45 //set __oom_handler 46 static void (* set_malloc_handler(void (*f)())) () 47 { 48 void (* old)() = __malloc_alloc_oom_handler; 49 __malloc_alloc_oom_handler = f; 50 51 return old; 52 } 53 }; 54 55 // init static func handler 56 template <int inst> 57 void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0; 58 59 template <int inst> 60 static void * __malloc_alloc_template<inst>::oom_malloc(size_t n) 61 { 62 void (* my_malloc_handler)(); 63 void * result; 64 65 for (;;) 66 { 67 my_malloc_handler = __malloc_alloc_oom_handler; 68 if (0 == my_malloc_handler) 69 { 70 __THROW_BAD_ALLOC; 71 } 72 (*my_malloc_handler)(); 73 result = malloc(n); 74 75 if (result) 76 { 77 return result; 78 } 79 } 80 } 81 82 template <int inst> 83 static void * __malloc_alloc_template::oom_realloc(void *p, size_t new_sz) 84 { 85 void (* my_realloc_handler)(); 86 void result; 87 88 for (;;) 89 { 90 my_realloc_handler = __malloc_alloc_oom_handler; 91 if (0 == my_realloc_handler) 92 { 93 __THROW_BAD_ALLOC; 94 } 95 96 (*my_realloc_handler)(); 97 98 result = realloc(p, new_sz); 99 if(result) 100 { 101 return result; 102 } 103 } 104 } 105 106 typedef __malloc_alloc_template<0> malloc_alloc;
2.当需要配置的区块 小于 128bytes时,调用第二级适配器
那么第二级适配器由哪些组成呢?
1:一个有16个单元的指针数组,每个单元中的指针指向一个链表,链表元素如下。
union obj
{
union obj * free_list_link;
char client_data[1];
}
这16个单元从0-15管理大小分别为8,16,24,...128bytes的小额区块,也就是每个单元只想的链表的元素大小分别为这些。
假如当申请一个大小为[1,8]或[16,24]大小的空间时,该配置器需从大小为8,24的链表中取一个元素来给客户端, 那么如何根据申请的大小来判断分配那种链表中的元素呢?,如下
enum {__ALIGN = 8};
static size_t FREELIST_INDEX(size_t bytes)
{
return ( ( (bytes) + __ALIGN - 1) / __ALIGN - 1);
}
可自行测试,例如申请7bytes的空间,带入后得到数组的index为0,即需要从该元素指针指向的链表申请空间,以此类推.
2:内存池。有了这样的维护不同大小的链表的数组,但是链表的各个元素的空间又由哪来的呢,std::alloc 还维护了一个内存池,也就是用两个指针一个只想内存池开头,另一个指向结尾,每当一个链表的元素用光时,当再次有请求改大小的链表元素时,
就会先向该内存池要空间,默认从该内存池中取出20个对象大小的空间,然后将这些空间在重新组织成链表的形式,放到数组中。
3:堆内存。当内存池中的空间用完后,便向堆申请空间。
4:若堆中的内存都没有了,那么这时候该怎么办呢?这时候就像链表元素更大的链表要空间.例如,当申请19bytes时,首先向元素大小为24的链表要空间,若没有了,想内存池要,若有,申请20*24的空间,然后
重新组织成链表形式放回数组,并分配1个空间,若内存池也没有了,那就向堆要空间,如果堆也没了,这时,想元素大小为32或更大的链表要空间,如果有的话就去除一个分配下去,然后把剩余的空间放到对应大小
的链表中,例如申请24bytes的时候,堆中也没有可用的了,那么这时需要向32以及更大的去要一个元素,这里假定是32的也没了,但是64的有空间,这时便从元素大小为64的链表中取一个下来,分给24给用户,剩下的40,放到元素大小为40的链表中。
总结起来就是 对应客户申请大小的链表->内存池 ->堆->元素大小更大的链表->内存不足处理程序.
本想介绍下二级配置器有哪些东西,一不小心把过程说了出来。
下边分析源码:
//下面是第二级配置器 246 //主要是维护一个内存池,用来小于128byte的小型区块内存的分配 247 //其中,有多个链表,各链表中的node大小从8-128byte,都是8的倍数 248 //分配时,不是8的倍数,上调至最近的8的倍数, 249 //然后从相应链表中取下一个对应大小的node分配给请求 250 #ifdef __SUNPRO_CC 251 enum {__ALIGN = 8}; //小型区块的上调边界,即次对于用户申请的空间大小n都要调整成最接近且大于n的8的倍数 252 enum {__MAX_BYTES = 128}; //用户申请的最大空间大小,若大于这个值,调用一级配置器 253 enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //数组的长度 254 #endif 255 256 //第二级配置器 257 template <bool threads, int inst> 258 class __default_alloc_template 259 { 260 private: 261 # ifndef __SUNPRO_CC 262 enum {__ALIGN = 8}; //小型区块的上调边界 263 enum {__MAX_BYTES = 128}; //小型区块的上限 264 enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; 265 # endif 266 //大小上调至8的倍数 267 static size_t ROUND_UP(size_t bytes) 268 { 269 return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); 270 } 271 __PRIVATE: 272 union obj 273 { 274 union obj * free_list_link; //用于在链表中指向下一个节点 275 char client_data[1]; //用于存储实际区块的内存地址,由于这是一个union,很好的节约了这个数据的内存 276 }; 277 private: 278 # ifdef __SUNPRO_CC 279 static obj * __VOLATILE free_list[]; 280 # else 281 static obj * __VOLATILE free_list[__NFREELISTS];//前面提到的那个有16个元素的数组,每个数组元素是个static obj* __VOLATILE,指向链表第一个元素 282 # endif 283 static size_t FREELIST_INDEX(size_t bytes) //此函数用来根据用户传来的bytes,找到对应数组元素的index 284 { 285 return (((bytes) + __ALIGN-1)/__ALIGN - 1); 286 } 287 288 //返回大小为n的对象,并可能加入大小为n的其他区块到free list 289 static void *refill(size_t n); 290 //配置一块空间,可容纳nobjs个大小为"size"的区块 291 //如果配置nobjs个区块有所不便,nobjs可能会降低 292 static char *chunk_alloc(size_t size, int &nobjs); 293 294 //chunk 分配、配置的状态 295 static char *start_free; //内存池起始位置。只在chunk_alloc()中变化 296 static char *end_free; //内存池结束位置。只在chunk_alloc()中变化 297 static size_t heap_size; //内存池空间不够时,向堆空间申请的大小 298 /* //初始化各个static变量 template <bool threads, int inst> 572 char *__default_alloc_template<threads, inst>::start_free = 0; //设置初始值 573 574 template <bool threads, int inst> 575 char *__default_alloc_template<threads, inst>::end_free = 0; //设置初始值 576 577 template <bool threads, int inst> 578 size_t __default_alloc_template<threads, inst>::heap_size = 0; //设置初始值 579 580 //初始化16种大小的区块链表为空 581 template <bool threads, int inst> 582 typename __default_alloc_template<threads, inst>::obj * __VOLATILE 583 __default_alloc_template<threads, inst>::free_list[ 584 # ifdef __SUNPRO_CC 585 __NFREELISTS 586 # else 587 __default_alloc_template<threads, inst>::__NFREELISTS 588 # endif 589 ] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
以上是除去锁以后的,加锁的以后讨论。下面看看二级配置器是如何配置空间的:
static void * allocate(size_t n) //std::alloc的申请函数 337 { 338 obj * __VOLATILE * my_free_list; 339 obj * __RESTRICT result; 340 341 //需要分配的大小大于二级配置器的__MAX_BYTES,直接使用第一级配置器 342 if (n > (size_t) __MAX_BYTES) 343 { 344 return(malloc_alloc::allocate(n)); 345 } 346 my_free_list = free_list + FREELIST_INDEX(n); //找到比需要分配的大小大,且最接近的大小块所在的链表所在free_list数组中的位置 347 352 result = *my_free_list; //取出找的对应链表的指向第一个节点的指针,插入也是从第一个插入,前插。 353 if (result == 0) //对应的链表中没有剩余未分配的节点区块 354 { 355 void *r = refill(ROUND_UP(n)); //再从内存池中分配一批,需求大小的区块(实际大小是请求大小上调至8的倍数后的数值), 356 //然后,放入对应链表,待分配给请求 357 return r; 358 } 359 //如果对应大小区块的链表中不为空,还有待分配的区块,取出第一个节点 360 *my_free_list = result -> free_list_link; 361 return (result); 362 }; 363 364 //p不可以是0 365 static void deallocate(void *p, size_t n) 366 { 367 obj *q = (obj *)p; 368 obj * __VOLATILE * my_free_list; 369 370 //大于区块大小上限的,直接调用第一级配置器释放 371 if (n > (size_t) __MAX_BYTES) 372 { 373 malloc_alloc::deallocate(p, n); 374 return; 375 } 376 my_free_list = free_list + FREELIST_INDEX(n); 377 382 //头插法,插入对应大小的区块链表 383 q -> free_list_link = *my_free_list; 384 *my_free_list = q; 385 } 387
可以看到,allocate()函数的过程如上所述,先从链表空间取,若链表为空,则去内存池去申请,调用的函数是 refill(ROUND_UP(n)),因为从内存池中获得的都是8的倍数,所以先将 n ROUND_UP一下。
下面是refill函数:
487 template <bool threads, int inst> 488 void* __default_alloc_template<threads, inst>::refill(size_t n) 489 { 490 int nobjs = 20; //默认一次分配20个需求大小的区块 491 char * chunk = chunk_alloc(n, nobjs); //到内存池中获取控件,chunk是分配的空间的开始地址,令其类型为char *,主要是因为一个char的大小正好是一个byte 492 obj * __VOLATILE *my_free_list; 493 obj * result; 494 obj * current_obj, * next_obj; 495 int i; 496 497 //如果只获得一个区块,这个区块就分配给调用者,free list 无新节点 498 if (1 == nobjs) return chunk;//nobjs开始定义为20,这里为什么要检查是否为1呢,原因是以传引用的方式穿到chunk_alloc,并且该函数会将njobs修改为实际申请到的数量 499 //否则准备调整free list,纳入新节点 500 my_free_list = free_list + FREELIST_INDEX(n); 501 502 //以下在chunk空间内建立free list 503 result = (obj *)chunk; //这一块准备返回给客端 504 // 以下导引free list 指向新配置的空间(取自内存池) 505 506 //由于chunk是char*,所以加上n,就表示走过n个char, 507 //一个char正好是一个byte,所以chunk+n现在指向第二个区块 508 *my_free_list = next_obj = (obj *)(chunk + n); 509 for (i = 1; ; ++i) 510 { 511 // 从1开始,因为第0个将返回给客端 512 current_obj = next_obj; 513 // 每次移动n个char,正好是n个byte,所以正好指向下个区块 514 next_obj = (obj *)((char *)next_obj + n); //下面讲下这个判断,假如从内存池中申请到了3个块的连续空间,上边的操作已经将第一个块空间返回个用户,那么只需要将剩下的两个换成链表形式,i表示已经被换成节点的个数,而 njobs表示总共个数,又由于第一个已经分配给了用户,所以只需处理njobs - 1个,那么nobjs - 1 == i 也就表示:是否将剩下的整块空间整理成的链表形式。 515 if (nobjs - 1 == i) 516 { 517 // 已经遍历完,此时next_obj指向的内存已经超出我们分配的大小了 518 // 不属于我们的内存 519 current_obj -> free_list_link = 0; 520 break; 521 } 522 else 523 { 524 current_obj -> free_list_link = next_obj; 525 } 526 } 527 return result; 528 }
那么chunk_alloc又是什么样的呢?:
template <bool threads, int inst> 401 char * 402 __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) 403 { 404 char * result; 405 size_t total_bytes = size * nobjs; 406 size_t bytes_left = end_free - start_free; 以上是关于STL容器内存配置器的主要内容,如果未能解决你的问题,请参考以下文章