C++的STL中的容器都是需要使用空间配置器来分配内存资源的
template<typename T, typename _Alloc=allocator<T>>
class vector
{
};
STL有很多版本,其中SGI STL是C++ STL的“民间版本”,因为其代码可读性很强。所以实际C++ STL和SGI STL是不同的。
C++ STL中的空间配置器默认是allocator
空间配置器的主要作用就是:
- 分离了对象的内存开辟和对象的构造
- 分离了对象的内存释放和对象的析构
空间配置器中主要包括了四个方法:
- allocate:负责容器的内存空间开辟
- deallocate:负责容器的内存空间释放
- construct:负责在容器中构造对象
- destroy:负责析构容器中的对象
SGI STL中的空间配置器有两个:一级allocator和二级allocator
一级allocator的内存管理是通过malloc/free实现的,二级allocator的内存管理是通过自定义内存池来实现的。
两种重要辅助函数_S_round_up(size_t)和_S_freelist_index(size_t)
// 计算出大于__bytes的最临近 8 的倍数的内存大小
size_t _S_round_up(size_t __bytes)
{
return ((_bytes) + (size_t)_AGLIN - 1) & ~((size_t)_AGLiN - 1);
}
// 在_S_free_list[]中找到可以分配__byte内存大小的freelist
size_t _S_freelist_index(size_t __bytes)
{
return ((_bytes) + (size_t)_AGLIN - 1) / (size_t)AGLIN - 1;
}
内存分配函数:allocate
/*
[0][1][2][3][...][14][15]
↓ ↓ ↓ ↓ ↓ ↓ ↓
当锁定到有对应大小内存块的freelist之后,每次分配freelist的头结点给外界
*/
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
// 当需要分配的空间大小大于128字节的时候,直接使用malloc进行分配
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
// 需要分配的空间<128字节
else {
// __my_free_list指向_S_free_list对应位置上的freelist
// 使用__my_free_list这个二级指针来遍历_S_free_list这个指针数组
// 找到可以分配_n内存大小的freelist
_Obj* __STL_VOLATILE* __my_free_list
= _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*/
// RAII管理锁,用OOP的方式来加解锁保证了allocate()的线程安全
// 栈上的对象出作用域之后自动析构的方式来释放锁
_Lock __lock_instance;
# endif
_Obj* __RESTRICT __result = *__my_free_list;
if (__result == 0)
// 如果freelist[index]上没有内存块,就分配内存块到该freelist上
__ret = _S_refill(_S_round_up(__n));
else {
// freelist[index]头结点的下一个节点成为头结点,而freelist原来的头结点分配给外界
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
构造freelist函数:_S_refill
// 分配_n大小的内存块,并将chunk块挂载到_S_free_list[_S_freelist_index(__n)]自由链表上
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
char* __chunk = _S_chunk_alloc(__n, __nobjs); // 给freelist分配chunk块
_Obj* __STL_VOLATILE* __my_free_list; // 用于遍历_S_free_list
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
// 如果只有一个chunk块的话,那么就直接返回出去
// 因为没有多余的chunk块了,所以也不需要将chunk块挂载到相应的freelist上
if (1 == __nobjs) return(__chunk);
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* Build free list in chunk */
// 分配一个chunk块出去,并将剩余chunk块挂载到*__my_free_list上
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 将剩余的内存块切分成chunk块大小的内存块并串联起来形成一条静态链表
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
// 如果当前chunk是链表中的最后一块,那么该节点就指向nullptr
// 否则当前位置的节点就指向下一个位置的chunk块
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
} else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result); // 链表的第一个节点被分配出去使用
}
为freelist提供内存块函数:_S_chunk_alloc
// 给对应的freelist申请大内存,为之后将大内存分割成以chunk为单位的静态链表做准备
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; // __nobjs个节点的内存大小
size_t __bytes_left = _S_end_free - _S_start_free; // 检测备用内存块是否有空间
/*
1.当对应freelist上的备用内存块很充足时,
将__bytes_left分成__nobjs个chunk块
2.当对应freelist上的备用内存块不是很充足,但是能够分配chunk块时,
__bytes_left内存块能分成多个chunk就分成多少个chunk块,但是只要能够分成一个chunk
3.当对应freelist上的备用内存块不能分配一个chunk块时,
使用malloc申请一个大内存块,然后分割成chunk块。没有用的内存块就当做备用内存块使用
注意:用于_S_start_free,_S_end_free,_S_heap_free都是静态变量
所以不用freelist上的备用内存块是可以混合起来使用的,并不是freelist只能
使用自己链表上的备用大内存块
*/
if (__bytes_left >= __total_bytes) {
// 分配出去__total_bytes大小的内存
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else if (__bytes_left >= __size) {
// 计算__bytes_left的内存块能够切分成多少个__size大小的chunk块,然后将能够整数分配的内存块进行分配
__nobjs = (int)(__bytes_left/__size); // __bytes_left能够分成多少个大小为__size的chunk块
__total_bytes = __size * __nobjs; // __total_bytes为能够整数切分chunk的总内存大小
// 分配出去__total_bytes大小的内存
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} else {
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 走到这里的__bytes_left大小的内存块一定是内存池中的内存碎片,需要被回收
// 将被分割成小块的内存 头插到有对应内存块大小的freelist中
// 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);
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
// malloc分配内存
_S_start_free = (char*)malloc(__bytes_to_get);
if (0 == _S_start_free) { // malloc分配异常
// 遍历_S_free_list[]中的freelist,判断是否存在freelist中的内存块比__size大
// 如果存在,就将该freelist上的大内存块分割成__size和另一个小内存块,并将分割下来的__size返回
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;
// 如果存在freelist中有大内存块,就将该内存块切割__size大小下来
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_free_list[]中的freelist中没有比__size大的内存块了
// 所以就会使用一级空间配置器重新申请大内存
_S_end_free = 0; // In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
// 更新freelist分配得到的内存大小_S_heap_size和freelist的尾指针_S_end_free
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs));
}
}
// 一级空间配置器的allocate就是通过malloc来开辟内存
static void* allocate(size_t __n)
{
// allocate底层就是用malloc开辟空间内存
void* __result = malloc(__n);
// 如果malloc申请内存失败,就会使用oom将有大内存块的进程杀死,并释放其占用的内存空间
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
// 用户提前注册的OOM回调函数
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
void (* __my_malloc_handler)(); // 定义一个函数指针
void* __result;
for (;;) {
// 用户可以提前注册一个__malloc_alloc_oom_handler回调函数
// 在系统的内存资源不够用的时候,会自动调用该回调,将指定的内存资源进行回收
__my_malloc_handler = __malloc_alloc_oom_handler;
// 如果注册了回调函数,就回调。否则就抛出异常
if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
(*__my_malloc_handler)(); // 调用OOM回调函数
// 进行了OOM之后,重新尝试执行malloc
// 如果任然malloc失败,就再次执行OOM回调函数,直到malloc申请内存成功
__result = malloc(__n);
if (__result) return(__result);
}
}
释放内存函数:deallocate
// 将__p位置上的,大小为__n的chunk块头插到对应的freelist,相当于将chunk块回收到内存池中了
/* __p may not be 0 */
static void deallocate(void* __p, size_t __n)
{
// 当__n大小超过128字节时,直接通过free进行释放
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
else {
// 在_S_free_list[]中找到能够存放__n大小的freelist
_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节点头插到_S_free_list[_S_freelist_index(__n)]自由链表上
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
// lock is released here,临界区结束
}
}
扩缩容内存函数: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;
// 如果__old_sz和__new_sz大于128字节的话,那么就使用库函数realloc进行扩缩容
if (__old_sz > (size_t) _MAX_BYTES && __new_sz > (size_t) _MAX_BYTES) {
return(realloc(__p, __new_sz));
}
// 如果__old_sz在内存对齐之后和__new_sz一样大,那么就不用进行内存的扩缩容
if (_S_round_up(__old_sz) == _S_round_up(__new_sz)) return(__p);
/*
申请__new_sz大小的内存,并把__p位置上数据拷贝到新地址处
如果__new_sz>__old_sz的话,那么就将旧数据全部拷贝到新地址处
如果__new_sz<__old_sz的话,那么就旧数据中__new_sz大小的部分拷贝到新地址处
*/
__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);
}
SGI STL二级空间配置器内存池优点
为了防止小块内存的频繁申请和释放造成的内存碎片问题,而严重的内存碎片问题会导致大内存块无法申请。所以一般会使用内存池进行小块内存的申请和释放。
一般的内存池:能够做到内存块按照一定字节数进行内存对齐,并分配内存。当申请内存的时候,就会找到内存对齐到指定的freelist上找chunk块(只会在自己的freelist上找内存块)。如果没有chunk就会直接malloc申请。如果malloc失败了,直接就系统崩溃了。
SGI STL的二级空间配置器的内存池相比而言:
- 找chunk块时,不仅会在自己的freelist和备用内存上找,也会去其他的freelist和备用内存块上找。并且如果备用内存过小的话,就将备用内存挂载到有相应的freelist上。
- malloc失败的时候,SGI STL的内存池有两种防护措施,一种是找大内存块,然后切割成小内存块。一种是通过OOM回调函数,强制回收内存,使得malloc不会失败。
总结SGI STL中二级空间配置器的内存池优点:
- 会申请大内存挂载到对应的freelist上,并将其中的一部分分割成chunk块串联起来形成一个静态链表,剩下的另一部分作为备份内存块放在freelist的下面。该内存块不仅可以供当前位置上的freelist使用,也可以给其他的freelist使用。
- 如果备用的内存块中只有很小的一块内存,并不够当前需要的chunk块大小的话,就会将这个小内存块挂载到对应freelist中。这样就可以充分利用备用的内存块。
- 当使用malloc申请大内存块失败的时候,会检查比申请的内存块大的chunk块,找到之后会将大内存块分割成执行内存块进行使用。如果没有大内存块可以分割了,也可以使用用户提前注册的OOM回调函数进行强制的指定内存回收。
SGI STL二级空间配置器内存池的不足
SGI STL的二级空间配置器中不同freelist可能指向自己申请的内存块,也可能指向其他freelist没有用完的备用内存块,这样就会导致freelist之间的指向关系很复杂。在多线程环境下,就需要使用大粒度的锁才能保证线程安全。
而tcmalloc中,虽然有三个cache,但是每一个cache中的哈希桶对应的内存块大小都是相同的,因此在加锁的时候,只需要对一个哈希桶进行加锁即可,这样就减少了加锁的粒度,从而减少了多线程竞争锁时的开销。
SGI STL中如果内存池中没有足够大小的内存块的话,首先会进行malloc直接分配,如果malloc失败了,才会从其他有大内存块的freelist中切割对应大小的内存块使用。另外如果没有大内存块的话,就会调用一级空间配置器的allocate,进而会调用用户提前注册的OOM回调函数,强制回收内存资源。
而tcmalloc中,如果对应大小的freelist中没有足够的内存块了,首先会去有大内存块的freelist上切割内存块,如果没有大内存块,才会进行malloc申请大内存块,然后再进行分割。