简单认识STL
STL是C++的一个标准库,提供六大组件:容器,算法,迭代器,仿函数,配接器,配置器。这里介绍的就是配置器。它负责空间配置与管理,分为两级,一级空间配置器和二级空间配置器。上一篇博客也提到过,当频繁的申请和释放小块内存空间的时候,会产生外碎片和效率低的问题。所以,STL把空间配置分为两级,大于128字节使用一级空间配置器,一级空间配置器只是简单的对系统的malloc和free做了一层封装。小于等于128字节使用二级空间配置器,也可以称为内存池。
一级空间配置器简单剖析
我这里拿到的是STL3.0版的,代码可读性较好。
一级空间配置器主要提供了allocate申请空间函数,deallocate释放空间函数,reallocate重新申请一块空间方法。除此之外,它模拟C++对于空间分配失败时的异常处理机制,在抛出异常之前,先调用一个错误处理函数。因此它还有一个set_malloc_handler函数,它的参数是一个函数指针,指向的就是内存分配失败要调用的函数,返回值也是一个函数指针。SGI的STL实际上没有这个内存不足的处理函数,不过它留了这个set_malloc_handler,你就可以自己实现一个处理内存不足的函数传进去。
看一下源码:
static void * allocate(size_t n)
{
void *result = malloc(n);
if (0 == result) result = oom_malloc(n);
return result;
}
没错,它直接调用的系统的malloc函数分配的,当分配失败时调用oom_malloc函数,那么这个函数是怎么回事呢?它会先试图去看有没有对内存不足进行处理的函数(系统默认是没有的,那函数指针就为NULL,直接抛出异常),如果定义了这个函数,那调用这个函数去解决内存不足的问题,然后再次尝试分配,如果还是失败,那继续重复上述工作,因为这是在一个死循环里进行的。源码如下:
void * __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) {
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);
}
}
deallocate更简单, 直接调用free:
static void deallocate(void *p, size_t /* n */)
{
free(p);
}
reallocate直接调用realloc函数。如果分配失败,机制和allocate一样,调用 oom_realloc去处理,而oom_realloc又和oom_malloc近乎是一样的。
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz);
if (0 == result) result = oom_realloc(p, new_sz);
return result;
}
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) {
my_malloc_handler = __malloc_alloc_oom_handler;
if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
(*my_malloc_handler)();
result = realloc(p, n);
if (result) return(result);
}
}
上边提到过的set_malloc_handler实现如下,参数和返回值都是函数指针类型,把原来的处理函数指针 __malloc_alloc_oom_handler保存下来,传进来的对内存不足处理的函数指针赋给原来的函数指针。这时候,系统就有了内存不足的处理函数。
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
一级空间配置器就是上面这个样子了,主要的特点是模拟了C++对于内存不足时的处理。
简洁版一级空间配置器
分析完它,也就可以自己实现一下了。
#include<iostream>
using namespace std;
//一级空间配置器
template<int inst>
class MallocAllocTemplate
{
typedef void(*MEMORYHANDLE)(); //重命名一个函数指针类型。
public:
static void * Allocate(size_t n)
{
void* result = malloc(n); //一级空间配置器直接调用malloc开辟空间
if (result == 0) //如果开辟失败
{
result = OomMalloc(n); //OomMalloc模拟C++对内存不足时的处理
}
return result;
}
static void Deallocate(void* ptr) //释放内存直接调用free
{
free(ptr);
}
static void * reallocate(void *p, size_t new_sz) //直接调用realloc开辟
{
void * result = realloc(p, new_sz);
if (0 == result)
result = oom_realloc(p, new_sz);
return result;
}
protected:
static void(*SetMallocHandler(MEMORYHANDLE f))() //参数是函数指针,返回类型是函数指针
{
MEMORYHANDLE old = __MallocAllocOomHandler; //原来的最后返回去
__MallocAllocOomHandler = f; //把传进来的处理函数给它
return(old);
}
static void* OomMalloc(size_t n)
{
void* result = NULL;
while (1)
{
MEMORYHANDLE MyMallocHandler = __MallocAllocOomHandler; //一个可能会对内存不足进行处理的函数的指针
if (MyMallocHandler == 0) //没有成功对内存不足进行处理
{
throw std::bad_alloc(""); //抛出异常
}
(*MyMallocHandler)(); //有处理内存不足的函数,则对内存不足进行处理。
result = malloc(n); //处理完毕再次尝试分配
if (result) //如果分配到了
{
return result;
}
}
}
private:
static MEMORYHANDLE __MallocAllocOomHandler;
};
template<int inst>
typename MallocAllocTemplate<inst>:: MEMORYHANDLE MallocAllocTemplate<inst>::__MallocAllocOomHandler = 0; //初始化不定义处理内存不足的函数,故函数指针初始化为0
二级空间配置器剖析
二级空间配置器相当于是一个内存池,它维护了16个自由链表,负责16种小型区块的的配置。如果自由链表里没有小块内存了,则去内存池里切分,如果内存池也没有了,就调用一级空间配置器填充内存池。如果需求块大于128字节则调用一级空间配置器。
二级空间配置器就不去看它的源码了,按照它源码的思路,我几乎写了一个差不多的,注释比较详细,通过这个来分析。
//二级空间配置器
template <bool threads, int inst>
class __DefaultAllocTemplate
{
public:
static void* Allocate(size_t n) //n为要申请的字节数
{
if (n > __MAX_BYTES) //如果超过128字节就使用一级空间配置器
{
return (MallocAllocTemplate<inst>::Allocate(n));
}
obj** myFreeList; //定义一个二级指针
obj* result;
//去找n字节对应自由链表的哪个位置,用myFreeList指向它
myFreeList = freeList + FreeListIndex(n); //封装一个函数去找n字节应该是链表的哪个位置
result = myFreeList;
if (result == 0) //如果当前没有内存块
{
void *r = refill(RoundUp(n)); //从大块内存里切分小块,RoundUp函数是在找n关于8字节对齐的那个数
return r;
}
*myFreeList = result->freeListLink; //让自由链表指向小块内存块的下一块,相当于头块内存被分配出去
return result;
}
static void Deallocate(void* ptr, size_t n) //还回来的指针和大小
{
obj* q = (obj*)ptr;
if (n > __MAX_BYTES) //大于128字节调用一级空间配置器的释放进行释放
{
MallocAllocTemplate<inst>::Deallcate(q);
return;
}
obj** myFreeList = freeList + FreeListIndex(n);
//还回来的内存块头插到自由链表
q->freeListLink = *myFreeList;
*myFreeList = q;
}
void* Reallocate(void* p, size_t oldsize, size_t newsize)
{
if (oldsize > __MAX_BYTES && newsize > __MAX_BYTES) //如果都大于128,直接调用系统的realloc
{
return realloc(p, newsize);
}
void* result;
size_t copySz;
if (oldsize == newsize) //如果要重新开辟的大小和原来的大小相等,直接返回原来的指针
{
return p;
}
result = Allocate(newsize); //调用内存池的分配函数分配newsize大小的空间
copySz = newsize > oldsize ? oldsize : newsize; //如果newsize比原来的大,就拷贝原来内存空间大小,如果比原来小,只需要拷贝新的内存大小的空间就行,防止内存越界访问
memcpy(result, p, copySz);
Deallocate(p, oldsize); //原来的内存还给内存池
return result;
}
private:
size_t RoundUp(size_t n)
{
//找n的8字节对齐数的高效做法
return ((n + __ALIGN - 1)&~(__ALIGN - 1));
}
//refill函数尝试从大块内存里切分20块,其中一块直接分配出去,19块挂在n字节对应的自由链表。
//如果内存池剩下的不够20块,则能分配多少就分配多少
void* refill(size_t n)
{
size_t nobj = 20;
char* chunk = chunkAlloc(n, nobj); //尝试切分20块n字节大小的内存
obj** myFreeList;
obj* result;
obj* curObj;
obj* nextObj
if (nobj == 1) //chunkAlloc函数在分配时可能只分配到了一个,此时直接把这个给外面用就行了
{
return(chunk);
}
//否则指向n字节对应的自由链表处
myFreeList = freeList + FreeListIndex(n);
//把分配过来的内存切成小块挂到自由链表
result = (obj*)chunk;
*myFreeList = nextObj = (obj*)(chunk + n); //从第二个小块开始链上去,把第二个小块的地址放进自由链表里
//第三个开始往下挂
for (size_t i = 1; i < nobj; i++)
{
curObj = nextObj;
nextObj = (obj*)((char*)nextObj + n); //相当于链表里的next=next->next.
curObj->freeListLink = nextObj; //相当于cur->next=next.
}
//出来设置尾的next为null
nextObj->freeListLink = NULL;
return result;
}
char* chunkAlloc(size_t bytes, size_t& nobj) //nobj传引用,万一不够20个,能分配多少是多少
{
char* result;
size_t totalBytes = bytes*nobj; //总的申请的字节数
size_t bytesLeft = endFree - startFree; //大块内存剩余的字节数
if (bytesLeft >= totalBytes) //剩下的内存满足需求
{
result = startFree;
startFree += totalBytes;
return result;
}
else if (bytesLeft >= bytes) //剩余的至少有一个小块的大小
{
nobj = bytesLeft / bytes; //计算能分配几个小块
totalBytes = nobj*bytes; //重新计算分配的总的字节的大小
result = startFree;
startFree += totalBytes;
return result;
}
else
{
size_t bytesToGet = 2 * totalBytes + RoundUp(heapSize >> 4);
if (bytesLeft > 0) //还有剩余一点的内存块
{
obj** myFreeList = freeList + FreeListIndex(bytesLeft); //找剩下的内存能挂到多大的内存小块上,因为取的时候是按照整数倍取的,所以bytesleft一定是8的整数倍
(obj*)startFree->freeListLink = *myFreeList;
*myFreeList = (obj*)startFree;
}
startFree = (char*)malloc(bytesToGet); //去系统申请内存空间
if (startFree == 0) //如果没有申请到
{
size_t i;
obj** myFreeList;
obj* p;
for (i = bytes; i <= __MAX_BYTES; i += __ALIGN) //回收之前分配出去的内存小块
{
myFreeList = freeList + FreeListIndex(i);
p = *myFreeList;
if (p != 0)
{
*myFreeList = p->freeListLink;
startFree = (char*)p;
endFree = startFree + i;
return (chunkAlloc(bytes, nobj));
}
}
endFree = 0; //小块内存也没有回收到。
startFree = (char*)MallocAllocTemplate<inst>::Allocate(bytesToGet);//尝试用一级配置器去申请空间
}
//在系统中申请到了空间
heapSize += bytesToGet;
endFree = startFree + bytesToGet;
return(chunkAlloc(bytes, nobj)); //内存池拿到了一大块内存,再去看是否够20个bytes的空间,进行切分。
}
}
size_t FreeListIndex(size_t n) //n字节对应于自由链表的下标
{
//为了减少除的次数的比较优化的做法。
return ((n + __ALIGN - 1) / __ALIGN - 1);
}
private:
enum { __ALIGN = 8 }; //对齐数
enum { __MAX_BYTES = 128 }; //最大字节数
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; //自由链表数
union obj //每一小块内存对象
{
obj* freeListLink;
char clientData[1];
};
private:
static obj* freeList[__NFREELISTS]; //指向小块内存的自由链表
static char* startFree; //大块空闲内存开始的位置
static char* endFree; //大块内存结束的位置
static size_t heapSize; //大块内存的大小
};
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;
template <bool threads, int inst>
typename __DefaultAllocTemplate<threads, inst>::obj*
__DefaultAllocTemplate<threads, inst>::freeList[] = { 0 };
deallocate释放的时候,大于128字节使用一级空间配置器释放,小于等于则把内存块还给自由链表。
refill函数在切分小块之前是通过chunkAlloc函数拿到的空间,chunkAlloc所做的事就是尝试给出20个所需块的总大小,如果内存池剩余的不够这么多,那如果至少能分配一个所需大小的空间它也返回。如果一个都不够的话,它会从自由链表里寻找是否还有没用的区块,找到了就拿出来,找不到就调用一级空间配置器。