网上有很多对于STL空间配置器源码的剖析,之所以这么多人去剖析空间配置器,我觉得是真的设计的太好,而且剖析空间配置器的架构的设计对于C++学者来说是一个不错的提高能力的项目,所以加入到这个解剖大军中来。
参照了侯捷的《STL源码剖析》,原本直接看源码不懂得东西,突然间豁然开朗。再次写下自己对于STL空间配置器的一点点理解。
要了解空间配置器,有一张图是必看的:
这张图是一级空间配置器宇二级空间配置器的封装方式与调用。从此图我们们可以看到其实空间配置器是分为两级的,而这里所谓的两级并没有高低之分,它们之间的区别就是看你想要申请内存空间的大小。如果申请的内存大小超过128,那么空间配置器就自动调用一级空间配置器。反之调用二级空间配置器。而且在这里要说明的是空间配置器默认使用的是一级空间配置器。
一. 一级空间配置器:
一级空间配置器就比较简单了,STL源码中的一级空间配置器命名为class __malloc_alloc_template ,它很简单,就是对malloc,free,realloc等系统分配函数的一层封装,我向这也是为什么这么取名的原因。
源码中的一级空间配置器也不难看懂,懂了他的思想也就不难写出如下的代码:
<span style="font-family:Microsoft YaHei;font-size:14px;">template<int inst>//非类型模板参数
class MallocAllocTemplate//一级空间配置器(malloc,free,realloc)
{
public:
static void* Allocate(size_t n)
{
void* ret = malloc(n);
if (0 == ret)
ret = OomMalloc(n);
return ret;
}
static void Deallocate(void* p)
{
free(p);
}
static void* Reallocate(void* p, size_t newsize)
{
void* ret = realloc(p, newsize);
if (ret == 0)
ret = OomRealloc(p, newsize);
return ret;
}
private:
static void* OomMalloc(size_t n)//调用自定义的句柄处理函数释放并分配内存
{
ALLOC_FUN hander;
void* ret;
while (1)
{
hander = MallocAllocHander;
if (0 == hander)
{
cout << "Out of memory" << endl;
exit(-1);
}
hander();
ret = malloc(n);
if (ret)
{
rteurn (ret);
}
}
}
static void* OomRealloc(void* p, size_t newsize)//同上
{
ALLOC_FUN hander;
void* ret;
while (1)
{
hander = MallocAllocHander;
if (0 == hander)
{
cout << "Out of memory" << endl;
exit(-1);
}
hander();
ret = realloc(p,newsize);
if (ret)
{
rteurn(ret);
}
}
}
static void(*SetMallocHandler(void(*f)()))();//设置操作系统分配内存失败时的句柄处理函数
static ALLOC_FUN MallocAllocHander;
};
template<int inst>
ALLOC_FUN MallocAllocTemplate<inst>::MallocAllocHander = 0;//句柄函数初始化为0</span>
一级空间配置器中没有可以讨论的,除了这个句柄函数:static void(*SetMallocHandler(void(*f)()))();对于一个C初学者来说想要看懂这个声明有点难度,这是一个返回值,参数都为函数指针的一个函数指针。填起来有点绕,其实他就是一个函数指针,它指向的是一个句柄函数,这个句柄函数对于一级空间配置器是比较重要的。
malloc,free,realloc等库函数是向系统申请内存并且操作的函数。平时我们并不太会遇到内存空间分配不出来的情况,但是如果这一套程序是运行在服务器上的,各种各样的进程都需要内存。这样频繁的分配内存,终有一个时候,服务器再也分配不出内存,那么空间配置器该怎么办呢?这个函数指针指向的句柄函数就是处理这种情况的设计。
MallocAllocHander()一般是自己设计的一种策略。这种策略想要帮助操作系统得到内存空间用以分配。所以,设计这个函数就是一个提升空间配置器效率的一个方法。一般是大牛去玩儿的。哈哈。如果并不像设计这个策略,就把句柄函数初始化为0.
二. 二级空间配置器:
一级空间配置器说起来比较乏味,他只是一层系统函数封装,真正酸爽的是二级空间配置器,里面有很多很棒的设计。多的不说,先来看二级空间配置器的框架,上代码:
<span style="font-family:Microsoft YaHei;font-size:14px;">template<bool threads,int inst>
class DefaultAllocTemplate//二级空间配置器
{
private:
enum{ ALIGN = 8 };
enum{ MAX_BYTES = 128 };
enum{ FREELISTSIZE = MAX_BYTES / ALIGN };
public:
static void* Allocate(size_t n)
{
if (n > MAX_BYTES)
{
return MallocAllocTemplate<inst>::Allocate(n);
}
void* ret = NULL;
size_t index = GetFreeListIndex(n);
if (FreeList[index])//自由链表上有内存块
{
obj* cur = FreeList[index];
ret = cur;
FreeList[index] = cur->listLink;
}
else //调用refill从内存池填充自由链表并返回内存池的第一个内存块
{
size_t bytes = GetRoundUpNum(n);
return Refill(bytes);
}
return ret;
}
static void* Reallocate(void* p, size_t oldsize, size_t newsize)
{
void* ret = NULL;
if (oldsize > (size_t)MAX_BYTES&&newsize > (size_t)MAX_BYTES)
return (realloc(p, newsize));
if (GetRoundUpNum(oldsize) == GetRoundUpNum(newsize))
return p;
ret = Allocate(newsize);
size_t copysize = oldsize > newsize ? newsize : oldsize;
memcopy(ret, p, copysize);
DeAllocate(p, oldsize);
return ret;
}
static void Deallocate(void* p, size_t n)
{
if (n > MAX_BYTES)//如果大于MAX_BYTES直接交还给一级空间配置器释放
return MallocAllocTemplate<inst>::Deallocate(p, n);
else//放回二级空间配置器的自由链表
{
size_t index = GetFreeListIndex(n);
obj* tmp = (obj*)p;
tmp->listLink = FreeList[index];
Freelist[index] = tmp;
}
}
public:
union obj
{
union obj* listLink;//自由链表中指向下一个内存快的指针
char clientData[1];//调试用
};
static size_t GetFreeListIndex(size_t bytes)//得到所需内存块在自由链表中的下标
{
return ((bytes + ALIGN - 1) / ALIGN - 1);
}
static size_t GetRoundUpNum(size_t bytes)//得到内存块大小的向上对齐数
{
return (bytes + ALIGN - 1)&~(ALIGN - 1);
}
static void* Refill(size_t n)//从内存池拿出内存填充自由链表
{
int nobjs = 20;//申请20个n大小的内存块
char* chunk = ChunkAlloc(n, nobjs);
if (nobj == 1)//只分配到一个内存
{
return chunk;
}
obj* ret = NULL;
obj* cur = NULL;
size_t index = GetFreeListIndex(n);
ret = (obj*)chunk;
cur = (obj*)(chunk + n);
//将nobj-2个内存块挂到自由链表上
FreeList[index] = cur;
for (int i = 2; i < nobjs; ++i)
{
cur->listLink = (obj*)(chunk + n*i);
cur = cur->listLink;
}
cur->listLink = NULL;
return ret;
}
static char* ChunkAlloc(size_t size, int& nobjs)
{
char* ret = NULL;
size_t Leftbytes = endFree - startFree;
size_t Needbytes = size * nobjs;
if (Leftbytes >= Needbytes)
{
ret = startFree;
startFree += Needbytes;
}
else if (Leftbytes >= size)//至少能分配到uoge内存块
{
ret = startFree;
nobjs = Leftbytes / size;
startFree += nobjs*size;
}
else //一个内存块都分配不出来
{
if (Leftbytes > 0)
{
size_t index = GetFreeListIndex(Leftbytes);
((obj*)startFree)->listLink = FreeList[index];
FreeList[index] = (obj*)startFree;
startFree = NULL;
}
//向操作系统申请2倍Needbytes加上已分配的heapsize/8的内存到内存池
size_t getBytes = 2 * Needbytes + GetRoundUpNum(heapSize >> 4);
startFree = (char*)malloc(getBytes);
if (startFree == NULL)//从系统堆中分配内存失败
{
for (int i = size; i < MAX_BYTES; i += ALIGN)
{
obj* head = FreeList[GetFreeListIndex(i)];
if (head)
{
startFree = (char*)head;
head = head->listLink;
endFree = startFree + i;
return ChunkAlloc(size, nobjs);
}
}
//最后的一根救命稻草,找一级空间配置器分配内存
//(其他进程归还内存,调用自定义的句柄处理函数释放内存)
startFree = MallocAllocTemplate<inst>::Allocate(getBytes);
}
heapSize += getBytes;//从系统堆分配的总字节数(可以用于下次分配时进行调节)
endFree = startFree + getBytes;
return ChunkAlloc(size, nobjs);//递归调用获取内存
}
return ret;
}
static obj* volatile FreeList[FREELISTSIZE];
static char* startFree;
static char* endFree;
static size_t heapSize;
};
//typename表示DefaultAllocTemplate<threads, inst>是一个类型,
//如果不标识,编译器对此模板一无所知
template<bool threads, int inst>
typename DefaultAllocTemplate<threads, inst>::obj* volatile
DefaultAllocTemplate<threads, inst>::FreeList[FREELISTSIZE] = { 0 };
template<bool threads, int inst>
char* DefaultAllocTemplate<threads, inst>::startFree = 0;
template<bool threads, int inst>
char* DefaultAllocTemplate<threads, inst>::endFree = 0;
template<bool threads, int inst>
size_t DefaultAllocTemplate<threads, inst>::heapSize = 0;</span>
这个代码是我自己提取出来的源码框架,但是他已经可以实现所有的功能。
首先需要说明的是二级空间配置器是由一个内存池和自由链表配合实现的。
<span style="font-family:Microsoft YaHei;font-size:14px;"> static obj* volatile FreeList[FREELISTSIZE];//维护自由链表
static char* startFree;//维护内存池
static char* endFree;</span>
srartFree就相当于水位线的一种东西,它标志着内存池的大小。
自由链表中其实是一个大小为16的指针数组,间隔为8的倍数。各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104, 112,120,128 字节的小额区块。在每个下标下挂着一个链表,把同样大小的内存块链接在一起。此处特别像哈希桶。
自由链表结构:
<span style="font-family:Microsoft YaHei;font-size:14px;"> union obj
{
union obj* listLink;//自由链表中指向下一个内存快的指针
char clientData[1];//调试用
};</span>
这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的时用户的数据。因此,allocator中的空闲块链表可以表示成:
obj* free_list[16];
obj* 是4个字节那么大,但是大部分内存块大于4。我们想要做的只是将一块块内存链接起来,我们不用看到内存里所有的东西,所以我们可以只用强转为obj*就可以实现大内存块的链接。
二级空间配置器是为频繁分配小内存而生的一种算法。其实就是消除一级空间配置器的外碎片问题。
操作系统频繁分配内存和回收内存的时候。这些6M,4M的小内存无法利用造成了外部碎片。
二级空间配置器就比较复杂了,现在我们来分析他的那些重要的函数:
Allocate()中:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre"> </span>static size_t GetFreeListIndex(size_t bytes)//得到所需内存块在自由链表中的下标
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>return ((bytes + ALIGN - 1) / ALIGN - 1);
<span style="white-space:pre"> </span>}</span>
此函数和源码中的FREELIST_INDEX(n)是一样的,它就是找到需要分配的内存块在自由链表中的什么地方,它的实现是((bytes + ALIGN - 1) / ALIGN - 1)。它其实是把药分配的内存大小提升一个数量级(+7,每间隔8为一个数量级),然后除以8,可以算到要找的内存块下标的下一个下标,减一,刚好久找到合适的下标处,取出一块内存块。
<span style="font-family:Microsoft YaHei;font-size:14px;"> static size_t GetRoundUpNum(size_t bytes)//得到内存块大小的向上对齐数
{
return (bytes + ALIGN - 1)&~(ALIGN - 1);
}</span>
此函数是得到所需内存块大小的向上对齐数。在自由链表中,我们的内存块大小总是8的倍数,但是并不是每次所需内存大小都是8的倍数。所以我们就要取比所需大小大或相等的内存块,这就是向上取整。&~(ALIGN - 1)相当于将低8位置0,只取高8位,高8位总是8的倍数,正好符合题意。
Allocate中最重要的两个函数static void* Refill(size_t n)和static char* ChunkAlloc(size_t size, int& nobjs):
<span style="font-family:Microsoft YaHei;font-size:14px;">static void* Refill(size_t n)//从内存池拿出内存填充自由链表
{
int nobjs = 20;//申请20个n大小的内存块
char* chunk = ChunkAlloc(n, nobjs);
if (nobj == 1)//只分配到一个内存
{
return chunk;
}
obj* ret = NULL;
obj* cur = NULL;
size_t index = GetFreeListIndex(n);
ret = (obj*)chunk;
cur = (obj*)(chunk + n);
//将nobj-2个内存块挂到自由链表上
FreeList[index] = cur;
for (int i = 2; i < nobjs; ++i)
{
cur->listLink = (obj*)(chunk + n*i);
cur = cur->listLink;
}
cur->listLink = NULL;
return ret;
}</span>
当在自由链表的下标处没有内存块时,我们就必须调用refill去填充自由链表。申请时一般一次性申请20个内存块大小的内存。通过移动startFree指针将内存池内的一段内存给“切割”出来,然后按照大小切成小块挂在自由链表下面。。返回第一块内存块给用户,其余的都挂在自由链表下,方便下次分配,根据局部性原理,这将极大地提升了分配内存空间的效率。
<span style="font-family:Microsoft YaHei;font-size:14px;">static char* ChunkAlloc(size_t size, int& nobjs)
{
char* ret = NULL;
size_t Leftbytes = endFree - startFree;
size_t Needbytes = size * nobjs;
if (Leftbytes >= Needbytes)
{
ret = startFree;
startFree += Needbytes;
}
else if (Leftbytes >= size)//至少能分配到一个内存块
{
ret = startFree;
nobjs = Leftbytes / size;
startFree += nobjs*size;
}
else //一个内存块都分配不出来
{
if (Leftbytes > 0)
{
size_t index = GetFreeListIndex(Leftbytes);
((obj*)startFree)->listLink = FreeList[index];
FreeList[index] = (obj*)startFree;
startFree = NULL;
}
//向操作系统申请2倍Needbytes加上已分配的heapsize/8的内存到内存池
size_t getBytes = 2 * Needbytes + GetRoundUpNum(heapSize >> 4);
startFree = (char*)malloc(getBytes);
if (startFree == NULL)//从系统堆中分配内存失败
{
for (int i = size; i < MAX_BYTES; i += ALIGN)
{
obj* head = FreeList[GetFreeListIndex(i)];
if (head)
{
startFree = (char*)head;
head = head->listLink;
endFree = startFree + i;
return ChunkAlloc(size, nobjs);
}
}
//最后的一根救命稻草,找一级空间配置器分配内存
//(其他进程归还内存,调用自定义的句柄处理函数释放内存)
startFree = MallocAllocTemplate<inst>::Allocate(getBytes);
}
heapSize += getBytes;//从系统堆分配的总字节数(可以用于下次分配时进行调节)
endFree = startFree + getBytes;
return ChunkAlloc(size, nobjs);//递归调用获取内存
}
return ret;
}</span>
ChunkAlloc要做的就是去找操作系统要内存,依次性要20个,但是我们要考虑很多情况:
- 内存池里有足够20块大的内存
- 内存池里有小于20块大于等于1块的内存大小
- 内存池里1块内存那么大都没有
- 空间配置器里所有的成员都是静态的。是为了在外面通过作用域就可以调用,而不需要构造对象。
- 空间配置器可以使用于大部分的数据结构,如List,vector等。
- 对于自由链表的初始化时特别容易错的。
template<bool threads, int inst>
typename DefaultAllocTemplate<threads, inst>::obj* volatile
DefaultAllocTemplate<threads, inst>::FreeList[FREELISTSIZE] = { 0 };
注意到typename了吗。它就为了完成一个功能,到苏编译器DefaultAllocTemplate<threads, inst>是一个类型,不然会出现如下错误:
这是难以发现的错误,编译器认识DefaultAllocTemplate<threads, inst>,报一个搞不懂的错误。