《STL源码分析》学习笔记 — 空间配置器 — pool_allocator
__pool_alloc 对应了书中所说的第一级配置器,其基类则对应第二级配置器。该配置器的工作目标是 减少小额区块造成的内存碎片。我们多次申请内存时,其在堆空间中的分配并不是连续的。因此,如果我们在堆中申请了许多小块的内存,就会造成当我们想要分配大内存时,系统找不到连续的内存以满足我们需要的空间。
我们这里使用的 cygwin 只提供了 gcc 的头文件。我们这里的函数实现有些是位于源文件中的。因此,我们将会使用 gcc10.2.0 的源码分析。
一、__pool_alloc_base
该基类定义了满足小额区块的内存上限(128bytes)。如果超过此上限,则交由派生类进行内存管理。当内存块大小不超过此上限时,则在内存池中进行内存管理(memory pool):
/**
* @brief Base class for __pool_alloc.
*
* 重要的实现性质:
* 0. 如果全局授权,则通过new分配内存
* 1. 如果用户要求的内存大于_S_max_bytes,直接从堆中分配内存
* 2. 在所有其它情况中,我们分配对象的大小为_S_round_up(requested_size)
* 这样用户持有足够的大小信息,使得我们可以将用户不再使用的对象防止到适当的 free-list中,而不会永远丢失该块内存
*/
class __pool_alloc_base
{
typedef std::size_t size_t;
protected:
enum {
_S_align = 8 }; // 对齐方式
enum {
_S_max_bytes = 128 }; // 小额区块的内存上限
enum {
_S_free_list_size = (size_t)_S_max_bytes / (size_t)_S_align }; // 维护的区块链表数量
union _Obj
{
union _Obj* _M_free_list_link; // 下一个链表节点
char _M_client_data[1]; // 用户看到的内存地址
};
static _Obj* volatile _S_free_list[_S_free_list_size]; // 自由链表数组
// 维护内存块状态
static char* _S_start_free; // 内存池中可用空间的起始地址
static char* _S_end_free; // 内存池中可用空间的结束地址
static size_t _S_heap_size; // 已经从堆中申请的总空间大小
size_t _M_round_up(size_t __bytes)
{
return ((__bytes + (size_t)_S_align - 1) & ~((size_t)_S_align - 1)); }
_GLIBCXX_CONST _Obj* volatile* _M_get_free_list(size_t __bytes) throw ();
__mutex& _M_get_mutex() throw ();
void* _M_refill(size_t __n);
char* _M_allocate_chunk(size_t __n, int& __nobjs);
};
1、_M_get_free_list
这个接口是用户获取指定 free-list 的接口。
// Definitions for __pool_alloc_base.
__pool_alloc_base::_Obj* volatile* __pool_alloc_base::_M_get_free_list(size_t __bytes) throw ()
{
size_t __i = ((__bytes + (size_t)_S_align - 1) / (size_t)_S_align - 1);
return _S_free_list + __i;
}
这里用到了我们在头文件中声明的对齐方式属性 _S_align。不难看出,这里计算得到的 i 实际上是 bytes 除以 _S_align 后向上取整并-1。这里-1是因为 i 作为数组下标使用。那么返回的值就是 _S_free_list 的第 i+1 个元素地址。
看到这里,我们就明白 _S_free_list 中实际上保存了指向 _S_align 不同倍数大小的 free-list 指针。其第一个元素保存的是大小为0-8的那些内存块,第二个是9-16,依此类推。_S_free_list 的类型是 union _Obj*。从其定义,不难看出它实际上是维护了数个链表。因此,我们可以得出一个结论:对于 _S_align 某个整数倍范围内(如25-32)的内存,如果内存池中有相应空间可分配,那么 _S_free_list 中对应下标的链表中的首元素一定是处于未使用状态的。
2、_M_round_up
此函数用于将某个数量扩展为8字节对其版本。
3、_M_allocate_chunk
// 从大的内存块(内存池)中(_S_start_free-_S_end_free)分配空间以避免碎片化。前提是充分利用空间
// __n 表示对象个数, __nobjs 表示每个对象大小(引用传递,以供派生类知道实际构造的对象个数)。
// 假设 __n 是适当对齐的,即为_S_align的整数倍。
// 这个函数要求调用者持有锁
char* __pool_alloc_base::_M_allocate_chunk(size_t __n, int& __nobjs)
{
char* __result; // 返回值
size_t __total_bytes = __n * __nobjs; // 需要的空间
size_t __bytes_left = _S_end_free - _S_start_free; // 当前空闲块中剩余的空间
if (__bytes_left >= __total_bytes)
{
// 如果当前空闲块中剩余的空间,分配并更新空闲块的起始地址
__result = _S_start_free;
_S_start_free += __total_bytes;
return __result ;
}
else if (__bytes_left >= __n)
{
// 如果当前空闲块中剩余的空间不满足空间要求,但是至少能满足分配给一个对象
// 计算能分配给几个对象及需要的空间,分配并返回
__nobjs = (int)(__bytes_left / __n);
__total_bytes = __n * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return __result;
}
else
{
if (__bytes_left > 0)
{
// 充分利用剩余的空闲块,将剩余的空间放到_S_free_list数组对应的链表中
// 从这里我们能看出来,这要求内存池中剩余的大小也是 _S_align 的整数倍,否则不能直接插入到某个内存池链表中
// 实现这个要求的方式就是:初始分配的内存时 _S_align 的整数倍,每次取对象时,其大小也是 _S_align 的整数倍
_Obj*