本系列文章更多是笔记形式,希望能在总结过程中将一些东西理顺。难免出错,欢迎指正。
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; //内存池剩余空间 407 408 if (bytes_left >= total_bytes) 409 { 410 //内存池中剩余的空间足够满足需求量 411 result = start_free; 412 start_free += total_bytes; 413 return(result); 414 } 415 else if (bytes_left >= size) 416 { 417 //内存池剩余空间不能完全满足需求量,但足够供应一个及以上的区块 418 nobjs = bytes_left/size; 419 total_bytes = size * nobjs; 420 result = start_free; 421 start_free += total_bytes; 422 return (result); 423 } 424 else 425 { 426 //内存池连一个区块的大小都无法满足,这时需要向堆中申请内存,但在这之前首先对内存池剩余的空间加以利用 427 size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); 428 //以下试着让内存池中的残余零头还有利用价值 429 if (bytes_left > 0) 430 { 431 //内存池中内还有一些零头,先配给适当的free list 432 //首先寻找适当的free list 433 obj * __VOLATILE * my_free_list = 434 free_list + FREELIST_INDEX(bytes_left); 435 436 //调整free list,将内存池中残余的空间编入 437 ((obj *)start_free) -> free_list_link = *my_free_list; 438 *my_free_list = (obj *)start_free; 439 } 440 441 //配置heap空间,用来补充内存池 442 start_free = (char *)malloc(bytes_to_get); 443 if (0 == start_free) 444 { 445 //如果heap空间不足,malloc()失败 446 int i; 447 obj * __VOLATILE *my_free_list, *p; 448 //当堆中内存也不够用时,需要向数组中节点大小更大的链表去要空间 452 for (i = size; i <= __MAX_BYTES; i += __ALIGN) 453 { 454 my_free_list = free_list + FREELIST_INDEX(i); 455 p = *my_free_list; 456 if (0 != p) 457 { 458 //free list内尚有未用区块 459 //调整free list以释放出未用的区块到内存池 460 *my_free_list = p -> free_list_link; 461 start_free = (char *)p; 462 end_free = start_free + i; 463 // 此时内存池已经有内存了 464 //修改istart_free和end_free后 递归调用自己,为了修正objs,同时如果你按照本函数的流程再走一遍的话,会发现其实只需一遍就可以完成任务, 465 return chunk_alloc(size, nobjs); 466 //注意,任何残余的零头终将被编入适当的free list中备用 467 468 } 469 } 470 end_free = 0; //如果出现意外(山穷水尽,到处都没有内存可用了) 471 //调用第一级配置器,之前一直没有理解,为什么当都没有内存后,需要调用第一级配置器,看过这边才明白一些。之前提到,第一级配置器中有个set_alloc_handler函数,也就是设置当内存不足时的处理函数,而这个函数做的最多的便是做一些搜集系统内可用的内存,然后可以在循环中调用申请空间的函数时能申请到空间等一些工作,详见<effective c++> 3rd中的条款49到52. 472 start_free = (char *)malloc_alloc::allocate(bytes_to_get); 473 //这会导致抛出异常,或内存不足的情况获得改善 474 } 475 heap_size += bytes_to_get; 476 end_free = start_free + bytes_to_get; 477 //递归调用自己,为了修正objs 478 return chunk_alloc(size, nobjs); 479 } 480 }
本人觉得STL的这个容器配置器最经典的:
1:整体的优化设计,分两层配置空间。
2:就是上边的 chunk_alloc 函数的设计,尤其是最后递归调用,自己整理自己。
之前一直没有理解,这次感觉理解了一些后觉得收获很大。
好了,最后在总结一些整个配置思路:
1.大于128的,直接调用一集配置器;
2.小于128的调用二级配置器:先找到合适的区块大小的链表要空间,若没有到内存池要,内存池没有到堆要,堆也没了,到数组其他的链表要,并整理取得区块的剩余的小空间,如果整个数组中都没有找到,就调用一级配置器,将其作为函数的最后的出口,其实主要是通过以及配置器中的
处理函数解决。
整个过程如上,具体细节可阅读代码,对着注释看,便一目了然。
之前讲过,第一级适配器中要设置处理函数,他恰巧可以作为了第二级配置器的出口,那么这个处理函数是怎么样的,具体做了哪些工作?如何设置等,见下篇文章。