SGI STL allocate源码刨析
文章目录
通过剖析SGI STL二级空间配置器内存池源码深入理解其实现原理
- 防止小块内存频繁的分配,释放,造成内存很多的碎片出来,内存没有更多的连续的大内存块。所以应用对于小块内存的操作,一般都会使用内存池来进行管理。
2.9版本
SGI STL包含了一级空间配置器和二级空间配置器,其中一级空间配置器allocator采用malloc和free来 管理内存,和C++标准库中提供的allocator是一样的,但其二级空间配置器allocator采用了基于freelist 自由链表原理的内存池机制实现内存管理。
空间配置器的相关定义
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc>
可以看到,容器的默认空间配置器是__STL_DEFAULT_ALLOCATOR( _Tp),它是一个宏定义,如下:
# ifndef __STL_DEFAULT_ALLOCATOR
# ifdef __STL_USE_STD_ALLOCATORS
# define __STL_DEFAULT_ALLOCATOR(T) allocator< T >
# else
# define __STL_DEFAULT_ALLOCATOR(T) alloc
# endif
# endif
从上面可以看到__STL_DEFAULT_ALLOCATOR通过宏控制有两种实现,一种是allocator< T >,另一种 是alloc,这两种分别就是SGI STL的一级空间配置器和二级空间配置器的实现。
template <int __inst>
class __malloc_alloc_template // 一级空间配置器内存管理类 -- 通过malloc和free管理内存
template <bool threads, int inst>
class __default_alloc_template { // 二级空间配置器内存管理类 -- 通过自定义内存池实现内存管理
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
// 考虑了线程安全问题
重要类型和变量定义
示意图
// 内存池的粒度信息
enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16};
// 每一个内存chunk块的头信息
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
// 组织所有自由链表的数组,数组的每一个元素的类型是_Obj*,全部初始化为0
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
// Chunk allocation state. 记录内存chunk块的分配情况 战备池
static char* start_free; // 指向pool的头
static char* end_free; // 指向pool的尾
static size_t heap_size; // 分配累计量
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_start_free = 0;
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_end_free = 0;
template <bool __threads, int __inst>
size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0;
重要的辅助接口函数
/*将 __bytes 上调至最邻近的 8 的倍数, 0 还是 0*/ —— 非常常用
static size_t _S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); } // enum {_ALIGN = 8};
/*返回 __bytes 大小的chunk块位于 free-list 中的编号*/
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1) / (size_t)_ALIGN - 1); }
枚举类型的大小会随着其中的元素的大小(最大容量 char、short和int等)而发生变化,先转成size_t (这里是unsigned int)
_S_round_up
- (size_t) _ALIGN-1) —— 0000 0000 0000 0111
- ~((size_t) _ALIGN - 1)) —— 1111 1111 1111 1000
- +7 然后 &,只保留了8以上的位数,非常巧妙
_S_freelist_index
- +7 然后 / 8 - 1
重要的函数接口
// 分配内存的入口函数
static void* allocate(size_t __n)
// 负责把分配好的chunk块进行连接,添加到自由链表当中
static void* _S_refill(size_t __n);
// 分配相应内存字节大小的chunk块,并且给下面三个成员变量初始化
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// 把chunk块归还到内存池
static void deallocate(void* __p, size_t __n);
// 内存池扩容函数
template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void* __p,
size_t __old_sz,
size_t __new_sz);
二级空间配置器 allocate函数
public:
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
_Obj* __STL_VOLATILE* __my_free_list // —— _Obj** 二级指针 + volatile
= _S_free_list + _S_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
_Obj* __RESTRICT __result = *__my_free_list; // 编号链的对应编号位置下指向其free list的空表第一个空位置的指针
if (__result == 0)
__ret = _S_refill(_S_round_up(__n)); // 分配内存池
else {
*__my_free_list = __result -> _M_free_list_link; // 指向下一个节点,嵌入式指针
__ret = __result;
}
}
return __ret; // 返回当前free list的第一个空闲位置
};
_S_refill函数
/* Returns an object of size __n, and optionally adds to size __n free list.*/
/* We assume that __n is properly aligned. */
/* We hold the allocation lock. */
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n) // n已通过ROUND_UP(n)调整至8的倍数,表示申请放置元素所需的单元区域的大小【即每一片片的大小】
{
int __nobjs = 20; // 预设每个free list切分取20个区块(若pool不足时可能取不够20块),
// 实际上这里最好可以设置为全局变量,这样方便以后想切分成30或更多区块时在全局下进行修改即可,而不是需要进入具体源码段
char* __chunk = _S_chunk_alloc(__n, __nobjs); // 调用chunk_alloc()函数生成一大段区块,且nobjs是pass-by-reference,
// 所以在内部操作时nobjs可能已被修改不再是20了!!
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
if (1 == __nobjs) return(__chunk);
__my_free_list = _S_free_list + _S_freelist_index(__n); // 首先取到对应的编号区域位置,其解引用存放的是free list空表的头端第一个位置
/* Build free list in chunk */
__result = (_Obj*)__chunk; // chunk为对应编号区创建出的free list的空表的头端地址
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n); // result取完位置后需要返回容器去放置元素对象的,free list空表的头端必须下移一位
// 所以*my_free_list解引用存放的指针必须下移一位即【chunk+n(因为每个区块相差的距离为元素类型的大小n),然后转化为obj*型】
for (__i = 1; ; __i++) { // for循环是将nobjs个小区块串接起来
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) { // 已经 串起 __nobj - 1 个 chunck 块
__current_obj -> _M_free_list_link = 0;
break;
} else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
}
_S_chunk_alloc
/* We allocate memory in large chunks in order to avoid fragmenting */
/* the malloc heap too much. */
/* We assume that size is properly aligned. */
/* We hold the allocation lock. */
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result;
size_t __total_bytes = __size * __nobjs; // 计算总需要申请的空间大小
size_t __bytes_left = _S_end_free - _S_start_free; // pool最终剩余的大小
if (__bytes_left >= __total_bytes) { // pool空间足够满足20块需求,此时并不需申请新的内存空间,所以不用更新heap_size
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else if (__bytes_left >= __size) { // pool空间只足以满足一块或以上需求,此时并不需申请新的内存空间,所以不用更新heap_size
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else {
// pool空间不足以满足一块分配需求,即pool为碎片空间(或者可能为0)
// 若为malloc返回0,则此时打算从对应编号区的右临近编号区的free list下取出一个空格区块
// (为什么往右取临近,因为右边的free list下的每个单元区块的容量都比当前编号区的单元区块容量大,
// 即能满足申请放置元素对象的大小要求!!)
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4); // 申请创建新的内存空间的总容量大小
// Try to make use of the left-over piece.
if (__bytes_left > 0) {
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left); // 剩余内存挂到对应的index下面
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
_S_start_free = (char*)malloc(__bytes_to_get); // 原 start_free 已经使用了
if (0 == _S_start_free) { // 若申请失败即已无内存时,则从对应编号区的右临近编号区的free list下取出一个空格区块
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __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.
for (__i = __size;
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
if (0 != __p) {
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
return(_S_chunk_alloc(__size, __nobjs));
// Any leftover piece will eventually make it to the
// right free list.
}
}
_S_end_free = 0; // In case of exception. 异常情况
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); // 使用第一级配置器,再次malloc
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs));
// 分配好pool空间后需要将free list的那份分割出去,而为了提高代码效率,不写重复代码,所以通过递归再试一次。
// 此时递归进入的肯定是 if(bytes_left >= total_bytes)或 if(bytes_left > size)的执行部分!
}
}
异常情况,调用一级配置器
deallocate
/* __p may not be 0 */
static void deallocate(void* __p, size_t __n)
{
// 若传入的指针p,满足其解引用的元素大小在0~128范围内,但p却不是有std::alloc生成的比如是malloc生成的
// 此时传入这里仍可进行处理,虽然其带有cookie但不影响,只是将指针指向修改以下而已
// 即此时std::alloc外的指针内存地址只要满足其解引用元素大小在0~128范围内均可被其回收使用,
// 只不过如果p所指大小不是8的倍数时,可能会导致灾难!!
if (__n > (size_t) _MAX_BYTES) // 传入的元素大小超出128,不属于std::alloc的操作范围,调用的是free
malloc_alloc::deallocate(__p, __n);
else {
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
// lock is released here
}
}
一级空间配置器 allocate
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
void (* __my_malloc_handler)(); // 不带形参的函数
void* __result;
for (;;) { // 不停循环,知道调用成功malloc 或者 抛出异常 为止
__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);
}
}
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;
#endif
reallocate 扩容或者缩容
使用的不多
template <bool threads, int inst>
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) { // _MAX_BYTES 128
return(realloc(__p, __new_sz));
}
if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) return(__p); // 调正到8的倍数是相同的,则不用重新分配
__result = allocate(__new_sz); // 申请 __new__sz
__copy_sz = __new_sz > __old_sz? __old_sz : __new_sz;
memcpy(__result, __p, __copy_sz);
deallocate(__p, __old_sz); // 释放 __old__sz
return(__result);
}
总结
SGI STL二级空间配置器内存池的实现优点:
-
对于每一个字节数的chunk块分配,都是给出一部分进行使用,另一部分作为备用,这个备用可以给当前字节数使用,也可以给其它字节数使用
-
对于备用内存池划分完chunk块以后,如果还有剩余的很小的内存块,再次分配的时候,会把这些小的内
存块再次分配出去,备用内存池使用的干干净净! -
当指定字节数内存分配失败以后,有一个异常处理的过程,bytes - 128字节所有的chunk块进行查看,如果哪个字节数有空闲的chunk块,直接借一个出去
-
如果上面操作失败,还会调用oom _malloc这么一个预先设置好的malloc内存分配失败以后的回调函数(一直for循环),没设置回调函数malloc失败则throwd __THROW_BAD_ALLOC 异常