网上有很多对于STL空间配置器源码的剖析,之所以这么多人去剖析空间配置器,我觉得是真的设计的太好,而且剖析空间配置器的架构的设计对于C++学者来说是一个不错的提高能力的项目,所以加入到这个解剖大军中来。
参照了侯捷的《STL源码剖析》,原本直接看源码不懂得东西,突然间豁然开朗。再次写下自己对于STL空间配置器的一点点理解。
要了解空间配置器,有一张图是必看的:
这张图是一级空间配置器宇二级空间配置器的封装方式与调用。从此图我们们可以看到其实空间配置器是分为两级的,而这里所谓的两级并没有高低之分,它们之间的区别就是看你想要申请内存空间的大小。如果申请的内存大小超过128,那么空间配置器就自动调用一级空间配置器。反之调用二级空间配置器。而且在这里要说明的是空间配置器默认使用的是一级空间配置器。
一. 一级空间配置器:
一级空间配置器就比较简单了,STL源码中的一级空间配置器命名为class __malloc_alloc_template ,它很简单,就是对malloc,free,realloc等系统分配函数的一层封装,我向这也是为什么这么取名的原因。
源码中的一级空间配置器也不难看懂,懂了他的思想也就不难写出如下的代码:
template//非类型模板参数
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
ALLOC_FUN MallocAllocTemplate::MallocAllocHander = 0;//句柄函数初始化为0
malloc,free,realloc等库函数是向系统申请内存并且操作的函数。平时我们并不太会遇到内存空间分配不出来的情况,但是如果这一套程序是运行在服务器上的,各种各样的进程都需要内存。这样频繁的分配内存,终有一个时候,服务器再也分配不出内存,那么空间配置器该怎么办呢?这个函数指针指向的句柄函数就是处理这种情况的设计。
MallocAllocHander()一般是自己设计的一种策略。这种策略想要帮助操作系统得到内存空间用以分配。所以,设计这个函数就是一个提升空间配置器效率的一个方法。一般是大牛去玩儿的。哈哈。如果并不像设计这个策略,就把句柄函数初始化为0.
二. 二级空间配置器:
一级空间配置器说起来比较乏味,他只是一层系统函数封装,真正酸爽的是二级空间配置器,里面有很多很棒的设计。多的不说,先来看二级空间配置器的框架,上代码:
template
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::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::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::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是一个类型,
//如果不标识,编译器对此模板一无所知
template
typename DefaultAllocTemplate::obj* volatile
DefaultAllocTemplate::FreeList[FREELISTSIZE] = { 0 };
template
char* DefaultAllocTemplate::startFree = 0;
template
char* DefaultAllocTemplate::endFree = 0;
template
size_t DefaultAllocTemplate::heapSize = 0;
首先需要说明的是二级空间配置器是由一个内存池和自由链表配合实现的。
static obj* volatile FreeList[FREELISTSIZE];//维护自由链表
static char* startFree;//维护内存池
static char* endFree;
srartFree就相当于水位线的一种东西,它标志着内存池的大小。
自由链表中其实是一个大小为16的指针数组,间隔为8的倍数。各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104, 112,120,128 字节的小额区块。在每个下标下挂着一个链表,把同样大小的内存块链接在一起。此处特别像哈希桶。
自由链表结构:
union obj
{
union obj* listLink;//自由链表中指向下一个内存快的指针
char clientData[1];//调试用
};
这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的时用户的数据。因此,allocator中的空闲块链表可以表示成:
obj* free_list[16];
obj* 是4个字节那么大,但是大部分内存块大于4。我们想要做的只是将一块块内存链接起来,我们不用看到内存里所有的东西,所以我们可以只用强转为obj*就可以实现大内存块的链接。
二级空间配置器是为频繁分配小内存而生的一种算法。其实就是消除一级空间配置器的外碎片问题。
操作系统频繁分配内存和回收内存的时候。这些6M,4M的小内存无法利用造成了外部碎片。
二级空间配置器就比较复杂了,现在我们来分析他的那些重要的函数:
Allocate()中:
static size_t GetFreeListIndex(size_t bytes)//得到所需内存块在自由链表中的下标
{
return ((bytes + ALIGN - 1) / ALIGN - 1);
}
此函数和源码中的FREELIST_INDEX(n)是一样的,它就是找到需要分配的内存块在自由链表中的什么地方,它的实现是((bytes + ALIGN - 1) / ALIGN - 1)。它其实是把药分配的内存大小提升一个数量级(+7,每间隔8为一个数量级),然后除以8,可以算到要找的内存块下标的下一个下标,减一,刚好久找到合适的下标处,取出一块内存块。
static size_t GetRoundUpNum(size_t bytes)//得到内存块大小的向上对齐数
{
return (bytes + ALIGN - 1)&~(ALIGN - 1);
}
Allocate中最重要的两个函数static void* Refill(size_t n)和static char* ChunkAlloc(size_t size, int& nobjs):
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)//至少能分配到一个内存块
{
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::Allocate(getBytes);
}
heapSize += getBytes;//从系统堆分配的总字节数(可以用于下次分配时进行调节)
endFree = startFree + getBytes;
return ChunkAlloc(size, nobjs);//递归调用获取内存
}
return ret;
}
ChunkAlloc要做的就是去找操作系统要内存,依次性要20个,但是我们要考虑很多情况:
这是难以发现的错误,编译器认识DefaultAllocTemplate