一级空间配置器之__malloc_alloc_template allocate 1 2 3 4 5 6 static void * allocate (size_t n) { void *result = malloc (n); if (0 == result) result = oom_malloc (n); return result; }
allocate 接口 本质就是调用 malloc 函数分配指定大小 n 的内存,如果分配内存失败,进入到 oom_malloc 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #if 0 # include <new> # define __THROW_BAD_ALLOC throw bad_alloc #elif !defined(__THROW_BAD_ALLOC) # include <iostream.h> # define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1) #endif static void (* set_malloc_handler(void (*f)())) () { void (* old)() = __malloc_alloc_oom_handler; __malloc_alloc_oom_handler = f; return (old); }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); } }
oom_malloc 接口 本质就是循环执行两个关键点,空间分配异常的回调函数 和 malloc 函数分配内存。
如果用户通过 set_malloc_handler 设置 __malloc_alloc_oom_handler 回调函数,那么即便分配空间失败(指 allocate 接口)也不会抛出异常,而是选择执行用户的回调函数。如果用户设置的回调函数不选择退出或抛出异常,那么下一步就是 继续尝试分配指定大小 n 内存。如此反复执行,直到 malloc 成功为止。
如果用户没有设置 __malloc_alloc_oom_handler 回调函数,那么就会抛出异常,亦不会尝试分配指定大小 n 内存,提早退出循环。
下图是第一种情况,至于第二种情况是在 执行默认的回调函数之后就退出,也不会到 malloc 阶段。
deallocate 1 2 3 4 static void deallocate (void *p, size_t ) { free (p); }
reallocate 1 2 3 4 5 6 static void * reallocate (void *p, size_t , size_t new_sz) { void * result = realloc (p, new_sz); if (0 == result) result = oom_realloc (p, new_sz); return result; }
reallocate 接口本质就是调用 realloc 重新分配指定大小空间。如果失败 进入 oom_realloc 接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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); } }
二级空间配置器之__default_alloc_template allocate 说明 联合体(共用体)及其链表的应用 在 C 语言中,联合体(union)是一种数据结构,它允许在同一内存位置存储不同的数据类型。与结构体不同,联合体的所有成员共享同一块内存,因此联合体的大小是其最大成员的大小。
1 2 3 4 5 union union_name { 数据类型 变量名; 数据类型 变量名; ... };
特点:
内存共享 :所有成员共享同一内存区域,联合体的大小等于其最大成员的大小。
只能存储一个值 :在任何时刻,联合体只能存储一个成员的值,修改一个成员的值会影响其他成员的值。
存储的最后一次存放的成员 :共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后,原有的成员就失去作用了。
以上 3 个变量在内存中占的字节数不同,但都从同一地址开始存放,也就是几个变量相互覆盖。这种使几个不同的变量共占同一段内存的结构。
我们来看 STL 中利用 union 定义,你在理解层面想象成链表的节点即可。
1 2 3 4 union obj { union obj * free_list_link; char client_data[1 ]; };
示意图:
将 bytes 调整为 _ALIGN 的最小整数倍 1 2 3 4 5 enum {__ALIGN = 8 }; static size_t ROUND_UP (size_t bytes) { return (((bytes) + __ALIGN-1 ) & ~(__ALIGN - 1 )); }
向上对齐为 __ALIGN 的倍数,如果你传递的 bytes 为 13,那么会向上取整为 16。
这个函数主要用于内存管理,确保分配的内存块按照特定的字节对齐。
返回 bytes 所在的下标 1 2 3 4 5 enum {__ALIGN = 8 }; static size_t FREELIST_INDEX (size_t bytes) { return (((bytes) + __ALIGN-1 )/__ALIGN - 1 ); }
根据区块大小,决定使用第 n 号 free-list 。
FREELIST_INDEX 可以根据传入的字节大小,方便找到合适的下标。比方说 传入的 30 字节,那么应该是在 下标 3 处,因为 下标 2 处 一个块的大小为 24,明显不合适。
1 (((bytes) + __ALIGN-1 )/__ALIGN - 1 ) = ((30 ) + 8 - 1 ) / 8 - 1 = 37 / 8 - 1 = 4 - 1 = 3
free_list 数组和枚举介绍 1 2 3 4 5 6 7 enum {__ALIGN = 8 };enum {__MAX_BYTES = 128 };enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; template <bool threads, int inst> __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 , };
__NFREELISTS = __MAX_BYTES/__ALIGN
,即 __NFREELISTS = 128 / 8 = 16。free_list 是一个 索引 0 ~15的数组 ,里面记录的 obj* 对象。
因此,我们讲 free_list + FREELIST_INDEX(n)
就是定位到合适块的可用的内存的起始地址。
二级空间配置器从零开始推导 第一次申请32字节 申请字节 n > 128 调用一级空间配置器,否则调用二级空间配置器。
为了方便讨论,假定一切都是最初的状态,用户第一次申请的字节大小为 30 字节。通过 FREELIST_INDEX 方法找到合适的块的下标。我们必然只能多给,不能少给,所以选择 下标 3 处,代表每次申请一个块的大小为 24 字节。
当前肯定是没有分配任何空间的,free_list 数组中的对象没有任何一个指向有效的内存区域。因此,会调用 refill 接口。调用该接口的时候,会调用 ROUND_UP 的方法把用户申请的目标字节数向上取整为 8 的倍数,因此传递进去的参数不是 30,而是 32。
还没有往下分析多少,就看到 chunk_alloc 接口,这是实际分配空间的接口,但我们需要重点知道传递给 chunk_alloc 的两个参数。n 是用户申请的字节大小,已被向上取整为 8 的倍数,即32。nobjs(局部变量) 是最初为 20,但不代表永远都是,后续的其他地方可能会被改动,但这是后话。
在确定传递进来的参数情况之后,下面两个变量的含义和值情况就明晰了。total_bytes = 32 * 20 = 640,bytes_left = 0。最初肯定是没有可用空间的,再者我们申请的字节数变为 640 字节了,尽管这依旧不是 STL 最终会申请的大小。
1 2 size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free;
由于 bytes_left = 0,暗示没有任何可用空间,因此进入到 else 逻辑中,申请真正的堆内存。如下就是 STL 会真正需要去申请的空间大小,即申请 40 个块 + heap_size / 16 之后的空间大小,即 1280 字节。
1 size_t bytes_to_get = 2 * total_bytes + ROUND_UP (heap_size >> 4 );
然后就回去申请堆内存,暂时不去考虑申请失败的情况。那么就会更新 heap_size的大小 和 end_free 的指向。然后继续递归调用 chunk_alloc。这时候就会执行到如下逻辑中:
1 2 3 4 5 6 7 8 9 char * result; size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free; if (bytes_left >= total_bytes) { result = start_free; start_free += total_bytes; return (result); }
当前示意图如下:
明明一个块就可以满足用户,但我们足足申请 40 个块。多申请就是为了避免反复申请,因为 malloc 是系统调用嘛。下面我们会看到 第一个块 会返回给用户,另外 19 个块会挂载到 free_list 数组下标 3 处,等到用户需要 32 字节点时候,直接来取。至于剩下的 20 块是为了后续给的时候也不必再去申请做准备了(不属于 free_list 数组下标 3 处,有可能会被挂载到其他下标处去,这个后面会有演示)。至此,我们可以看到 start_free 指向剩余空间的起始位置,end_free 指向剩余空间的末尾位置,end_free - start_free 就代表剩下的可分配空间(因为它现在是自由的,不属于任何一个 free_list 数组)。
这个时候,我们就返回到 refill 中,由于申请的得到的是 20 个块,因此不会直接返回,而是要去构建链表。先把第一个节点存储,这是后面要返回给用户的。剩下的 19 个块 会通过指针连起来,和链表一样。而且最后一个节点指向的空指针,源码可见 current_obj -> free_list_link = 0
。
对于剩下的 19 个块链接起来之后,会挂载到下标为 3 的 free_list 中,源码可见 *my_free_list = next_obj = (obj *)(chunk + n)
。
然后就回到 allocate 中,由于已经申请到空间,直接就返回刚刚得到的第一个块,非常顺利。如果下次继续调用 allocate 申请 30 字节空间,它会跳过 refill 代码,因为目前有足够的堆空间,执行的是如下代码:
1 2 3 4 5 6 7 8 9 10 static void * allocate (size_t n) { obj * __VOLATILE * my_free_list; obj * __RESTRICT result; my_free_list = free_list + FREELIST_INDEX (n); result = *my_free_list; *my_free_list = result -> free_list_link; return (result); };
这个时候它是如何取处第二个块,并且返回给用户的呢?核心代码就是 result -> free_list_link
。你可以理解这行代码就是链表中 node->next 的行为。
示意图如下:
为了方便后面讨论,我们做出如下规定。
前面演示从没有分配空间到构建出内存池的过程,期间我们忽略很多逻辑,下面我们需要把它补回来,但我们确实是走完 allocate 的流程了,剩下 refill 和 chunk_alloc 的还有些没有走。
下面这个路线,即从用户本欲申请 30 字节,到 STL 实际分配大小的情况:
第二次申请64字节 这次我们申请 64 字节,发现 下标 7 处为空指针,因此调用 refill,然后就是调用 chunk_alloc。
1 2 size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free;
64 < bytes_left < total_bytes,进入的逻辑是:
1 2 3 4 5 nobjs = bytes_left / size; total_bytes = size * nobjs; result = start_free; start_free += total_bytes;return (result);
就是把之前多申请的 20 个块,用来弥补这里,示意图如下:
第三次申请96字节 之前申请的 20 个 32 字节的块都已分配完,start_free = end_free = 0,可用空间为 0 了。
这次计算 bytes_to_get 结果有所不同,因为 heap_size 是全局变量,会不止申请 20 个 96 字节块。计算 bytes_to_get = 2 * 96 * 20 + 80 = 3920。
1 size_t bytes_to_get = 2 * total_bytes + ROUND_UP (heap_size >> 4 );
示意图如下:
最后申请72字节,认为内存池和堆空间都没有连续的72字节 这次我们申请 72 字节,发现 下标 8 处为空指针,因此调用 refill,然后就是调用 chunk_alloc。
1 2 size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free;
由于我们认为内存池和堆空间都没有连续的72字节,目的是为了把另外两份代码展示出来。
bytes_left 小于 72 字节且大于 0 的话,通过下面的方式 让 start_free 空指针,即 free_list[8]。
1 2 3 4 5 6 7 size_t bytes_to_get = 2 * total_bytes + ROUND_UP (heap_size >> 4 ); 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 分配失败,才得以进入下面的代码中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 start_free = (char *)malloc (bytes_to_get);if (0 == start_free) { int i; obj * __VOLATILE * 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 = 0 ; start_free = (char * ) malloc_alloc: :allocate (bytes_to_get); }
起始字节数是 72,因为这是我们的目标,我们会从下标 8 的 free_list 数组中开始往后寻找,由于小于72字节的空间可能不是连续的,所以需要向后面大于72字节的链表”借”。
如果 free_list 数组的下标处为 NULL,表明不可能有空间给我们。否则,我们认为必然可以满足 72 字节需求。
如果遍历 free_list 的结果是没有任何可用的内存池,那么我们 将 end_free 置为 0,而 start_free 当前已经是 0,最后我们调用一级空间配置器,如果这都失败,就会抛出异常,这个我们在之前已经讨论过了。
如果我们遍历 free_list 有结果,进入下面的逻辑中:
1 2 3 4 * my_free_list = p - >free_list_link; start_free = (char * ) p; end_free = start_free + i;return (chunk_alloc (size, nobjs));
p 是 满足题意的下标的起始地址,也是获取可用空间的一个块,此时 start_free 和 end_free 情况如下:
然后递归调用 chunk_alloc。下面是两个变量最新的值情况。
1 2 size_t total_bytes = size * nobjs; size_t bytes_left = end_free - start_free;
由于 bytes_left >= size,进入到下面的逻辑代码中。
1 2 3 4 5 nobjs = bytes_left / size; total_bytes = size * nobjs; result = start_free; start_free += total_bytes; return (result);
效果就是把 96字节 切割出 72字节返回,剩下 24 字节。
然后让 start_free 和 end_free 记录着剩下的 24 字节,留着后续使用。
回到 refill 函数,就会执行如下代码,这个代码此前还没有演示过。
1 if (1 == nobjs) return (chunk);
至此,我们终于把 alloacte 的所有源代码拿下了。
deallocate 1 2 3 4 5 6 7 8 9 10 11 12 13 static void deallocate (void *p, size_t n) { obj *q = (obj *)p; obj * __VOLATILE * 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; }
如果 释放的字节数 大于 128,调用之前的讲的 deallocate。否则,就把要释放的空间重新归还到合适的 free_list 下标处,通过 free_list + FREELIST_INDEX(n)
计算。
申请空间:
归还空间:
reallocate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void * __default_alloc_template<threads, inst>::reallocate (void *p, size_t old_sz, size_t new_sz) { void * result; size_t copy_sz; if (old_sz > (size_t ) __MAX_BYTES && new_sz > (size_t ) __MAX_BYTES) { return (realloc (p, new_sz)); } if (ROUND_UP (old_sz) == ROUND_UP (new_sz)) return (p); result = allocate (new_sz); copy_sz = new_sz > old_sz? old_sz : new_sz; memcpy (result, p, copy_sz); deallocate (p, old_sz); return (result); }
如果新分配空间的长度和旧分配空间的长度都 大于 128,调用本小节讲的 realloc 接口。
如果新分配空间的长度和旧分配空间 向上取整为 8 的倍数,比较相等,那么直接返回指针 p 即可。
否则,就调用 allocate 分配 new_sz 长度的空间大小,确定要拷贝的长度 copy_sz ,调用 memcpy 即可。
对于不在使用的空间,记得归还,调用本小节讲的 deallocate 即可。
construct 1 2 3 4 template <class T1 , class T2 >inline void construct (T1* p, const T2& value) { new (p) T1 (value); }
参数 p 是一个 T1 类型的指针,也就是通过 allocate 分配的实际空间的起始地址。参数 value 就是要拷贝到该空间的数据,即作为要构造 T1 对象的初始值。
因此,该函数的功能就是在指定内存地址上构造对象。
destroy 1 2 3 4 template <class T >inline void destroy (T* pointer) { pointer->~T (); }
就是调用对象的析构函数。
空间配置器的使用 四个重要函数 1 2 3 4 5 6 7 8 9 10 11 用于申请一块未初始化的内存。T* allocate (std::size_t n) :申请一块大小为 n 个 T 类型的未初始化内存块,返回指向这块内存的指针。 用于释放先前分配的内存块。 void deallocate (T* p, std::size_t n) :释放指向 p 的内存,这块内存大小为 n 个 T 类型。用于在分配的内存上构造对象。 void construct (pointer p, const_reference val) :在指针 p 所指向的内存位置上构造一个对象,其值为 val。用于销毁对象。 void destroy (pointer p) :销毁指针 p 所指向的对象,但不释放内存。
容器源码内部在涉及内存和对象相关操作的时候,就是调用此四个接口。
为什么要将空间的申请和对象的构建分开 因为在 STL 中元素的个数往往都批量创建,这个时候如果创建一个对象申请一块空间,那么空间的申请就会非常频繁,效率自然低下。况且你还不能保证一个一个去申请,最后的内存是否连续的情况。
因此,一开始就创建指定大小的空间,再去构建对象能避免多次申请空间出现的内存碎片问题。
内存基本处理工具 uninitialized_fill_n uninitialized_fill_n
–> __uninitialized_fill_n
–> __uninitialized_fill_n_aux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <class ForwardIterator , class Size , class T >inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __true_type) { return fill_n (first, n, x); }template <class ForwardIterator , class Size , class T > ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __false_type) { ForwardIterator cur = first; __STL_TRY { for ( ; n > 0 ; --n, ++cur) construct (&*cur, x); return cur; } __STL_UNWIND(destroy (first, cur)); }
这里面有个参数值得去细究,即__false_type
。下面看看它是如何一步一步传递过来的:
1 2 3 4 5 6 7 8 9 10 11 12 13 template <class ForwardIterator , class Size , class T , class T1 >inline ForwardIterator __uninitialized_fill_n(ForwardIterator first, Size n, const T& x, T1*) { typedef typename __type_traits<T1>::is_POD_type is_POD; return __uninitialized_fill_n_aux(first, n, x, is_POD ()); }template <class ForwardIterator , class Size , class T >inline ForwardIterator uninitialized_fill_n (ForwardIterator first, Size n, const T& x) { return __uninitialized_fill_n(first, n, x, value_type (first)); }
追溯:value_type(first)
作用是获取 first
所指向元素的类型。
1 2 template <class T >inline T* value_type (const T*) { return (T*)(0 ); }
追溯:is_POD()
用来判断类型 T1
是否是一个 POD 类型。
1 2 3 4 5 6 7 8 __type_traits<T1>::is_POD_type ↓ __false_type ↓ struct __true_type { }; struct __false_type { };
如果是 POD 类型(如内置类型 int
、double
等),则使用下面这个函数完成初始化,而无需调用构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 template <class ForwardIterator , class Size , class T >inline ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __true_type) { return fill_n (first, n, x); }template <class OutputIterator , class Size , class T >OutputIterator fill_n (OutputIterator first, Size n, const T& value) { for ( ; n > 0 ; --n, ++first) *first = value; return first; }
否则,会通过调用 construct
函数手动构造对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <class ForwardIterator , class Size , class T > ForwardIterator __uninitialized_fill_n_aux(ForwardIterator first, Size n, const T& x, __false_type) { ForwardIterator cur = first; __STL_TRY { for ( ; n > 0 ; --n, ++cur) construct (&*cur, x); return cur; } __STL_UNWIND(destroy (first, cur)); }template <class T1 , class T2 >inline void construct (T1* p, const T2& value) { new (p) T1 (value); }
uninitialized_copy uninitialized_copy
–> __uninitialized_copy
–> __uninitialized_copy_aux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template <class InputIterator , class ForwardIterator >inline ForwardIterator __uninitialized_copy_aux(InputIterator first, InputIterator last, ForwardIterator result, __true_type) { return copy (first, last, result); }template <class InputIterator , class ForwardIterator > ForwardIterator __uninitialized_copy_aux(InputIterator first, InputIterator last, ForwardIterator result, __false_type) { ForwardIterator cur = result; __STL_TRY { for ( ; first != last; ++first, ++cur) construct (&*cur, *first); return cur; } __STL_UNWIND(destroy (result, cur)); }
其余部分代码,已不在话下,重点追溯 copy(first, last, result)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <class InputIterator , class OutputIterator >inline OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result) { return __copy_dispatch<InputIterator,OutputIterator>()(first, last, result); }inline char * copy (const char * first, const char * last, char * result) { memmove (result, first, last - first); return result + (last - first); }inline wchar_t * copy (const wchar_t * first, const wchar_t * last, wchar_t * result) { memmove (result, first, sizeof (wchar_t ) * (last - first)); return result + (last - first); }
__copy_dispatch
–> __copy
1 2 3 4 5 6 7 8 template <class InputIterator , class OutputIterator >inline OutputIterator __copy(InputIterator first, InputIterator last, OutputIterator result, input_iterator_tag) { for ( ; first != last; ++result, ++first) *result = *first; return result; }
还有另一个重载的函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <class RandomAccessIterator , class OutputIterator >inline OutputIterator __copy(RandomAccessIterator first, RandomAccessIterator last, OutputIterator result, random_access_iterator_tag) { return __copy_d(first, last, result, distance_type (first)); }template <class RandomAccessIterator , class OutputIterator , class Distance >inline OutputIterator __copy_d(RandomAccessIterator first, RandomAccessIterator last, OutputIterator result, Distance*) { for (Distance n = last - first; n > 0 ; --n, ++result, ++first) *result = *first; return result; }
明显看出这个函数实现的功能是复制指定长度的数据,而前一个是拷贝所有的数据。
uninitialized_fill uninitialized_fill
–> __uninitialized_fill
–> __uninitialized_fill_aux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template <class ForwardIterator , class T >inline void __uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __true_type) { fill (first, last, x); }template <class ForwardIterator , class T >void __uninitialized_fill_aux(ForwardIterator first, ForwardIterator last, const T& x, __false_type) { ForwardIterator cur = first; __STL_TRY { for ( ; cur != last; ++cur) construct (&*cur, x); } __STL_UNWIND(destroy (first, cur)); }
有之前的基础,这份源码也不再话下,即便是 fill 函数也没什么可讲之处。
1 2 3 4 5 template <class ForwardIterator , class T >void fill (ForwardIterator first, ForwardIterator last, const T& value) { for ( ; first != last; ++first) *first = value; }