版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xy913741894/article/details/66974004
目录(?)[+]
我们知道,stl有容器,空间配置器,适配器,迭代器,仿函数以及算法这6个组件,它们六者关系大概如下:容器通过配置器取得数据存储空间,算法通过迭代器获取容器内容,仿函数可以协助算法完成不同的策略变化,配接器可以修饰或套界仿函数。
侯捷在《STL源码剖析》一书讲到:
因此我们需要先去学习空间配置器。
一般来说,我们习惯的C++内存配置和释放操作是这样的:
class A {};
A* pa = new A;
//...执行其他操作
delete pa;
这里面隐含几个操作,对于new,我们都是先配置内存,然后调用对应的构造函数;而delete则是先调用对应的析构函数,然后释放内存。
在SGI版本的STL中,空间的配置释放都由< stl_alloc.h > 负责。它的设计思想如下:
PS:关于内存碎片,下图可以解释:
由于以上的问题,SGI设计了两层的配置器,也就是第一级配置器和第二级配置器。同时为了自由选择,STL又规定了 __USE_MALLOC 宏,如果它存在则直接调用第一级配置器,不然则直接调用第二级配置器。SGI未定义该宏,也就是说默认使用第二级配置器
需要注意的是,SGI版STL提供了一层更高级的封装,定义了一个simple _ alloc类,无论是用哪一级都以模板参数alloc传给simple _ alloc,这样对外体现都是只是simple _ alloc
而它的代码实现比较简单,仅仅是调用一级或者二级配置器的接口
template<class T, class Alloc = AllocToUse>
class SimpleAlloc
{
public:
static T* Allocate()
{
return (T*)Alloc::Allocate(sizeof(T));
}
static T* Allocate(size_t n)
{
return n == 0 ? 0 : (T*)Alloc::Allocate(n * sizeof(T));
}
static void Deallocate(T* p)
{
if (p != NULL)
return Alloc::Deallocate(p, sizeof(T));
}
static void Deallocate(T* p, size_t n)
{
return Alloc::Deallocate(p, n * sizeof(T));
}
};
直接调用malloc和free来配置释放内存,简单明了。
template<int Inst>
class __MallocAllocTemplate //一级空间配置器
{
typedef void (*OOM_HANDLER)();
private:
//these funs below are used for "OOM" situations
//OOM = out of memory
static void* OOM_Malloc(size_t n); //function
static void* OOM_Realloc(void *p, size_t newSZ); //function
static OOM_HANDLER OOM_Handler; //function pointer
public:
static void* Allocate(size_t n)
{
void* ret = malloc(n);
if (ret == NULL)
ret = OOM_Malloc(n);
return ret;
}
static void Deallocate(void* p, size_t n)
{
free(p);
}
static void* Reallocate(void* p, size_t oldSZ, size_t newSZ)
{
void* ret = realloc(p, newSZ);
if (ret == NULL)
ret = OOM_Realloc(p, newSZ);
return ret;
}
//static void (* set_malloc_handler(void (*f)()))()
//参数和返回值都是函数指针void (*)()
static OOM_HANDLER SetMallocHandler(OOM_HANDLER f)
{
OOM_HANDLER old = OOM_Handler;
OOM_Handler = f;
return old;
}
};
//让函数指针为空
template<int Inst>
void (*__MallocAllocTemplate::OOM_Handler)() = NULL;
template<int Inst>
void* __MallocAllocTemplate::OOM_Malloc(size_t n)
{
void* ret = NULL;
void(*myHandler)() = NULL;
for (;;)
{
myHandler = OOM_Handler;
if (myHandler == NULL)
throw bad_alloc();
(*myHandler)();
ret = malloc(n);
if (ret != NULL)
return ret;
}
}
template<int Inst>
void* __MallocAllocTemplate::OOM_Realloc(void* p, size_t newSZ)
{
void* ret = NULL;
void(*myHandler)() = NULL;
for (;;)
{
myHandler = OOM_Handler;
if (myHandler == NULL)
throw bad_alloc();
(*myHandler)();
ret = realloc(p, newSZ);
if (ret != NULL)
return ret;
}
}
typedef __MallocAllocTemplate<0> MallocAlloc; //一级空间配置重命名
根据情况来判定,如果配置区块大于128bytes,说明“足够大”,调用第一级配置器,而小于等于128bytes,则采用复杂内存池(memory pool)来管理。
图示如下:
第二级空间配置器的过程,我们重点可以看allocate和deallocate这两个函数的实现
static void* Allocate(size_t n)
{
if (n > (size_t)__MAX_BYTES) // 字节数大于128,调用一级空间配置器
return MallocAlloc::Allocate(n);
//不然到freelist去找
Obj* volatile* myFreeList = FreeList + FreeListIndex(n); //定位下标
Obj* ret = *myFreeList;
if (ret == NULL)
{
void* r = Refill(RoundUP(n));//没有可用free list 准备装填
}
*myFreeList = ret->freeListLink;
return ret;
}
可以看出来:
static void Deallocate(void* p, size_t n)
{
if (n > (size_t)__MAX_BYTES) //区块大于128, 则直接由第一级空间配置器收回
MallocAlloc::Deallocate(p, n);
Obj* volatile* myFreeList = FreeList + FreeListIndex(n);
Obj* q = (Obj*)p;
q->freeListLink = *myFreeList;
*myFreeList = q;
}
释放操作和上面有点类似:
如上图所示,自由链表是一个指针数组,有点类似与hash桶,它的数组大小为16,每个数组元素代表所挂的区块大小,比如free _ list[0]代表下面挂的是8bytes的区块,free _ list[1]代表下面挂的是16bytes的区块…….依次类推,直到free _ list[15]代表下面挂的是128bytes的区块
同时我们还有一个被称为内存池地方,以start _ free和 end _ free记录其大小,用于保存未被挂在自由链表的区块,它和自由链表构成了伙伴系统。
我们之前讲了,如果用户申请小于等于128的区块,就到自由链表中取,但是如果自由链表对应的位置没了怎么办???这下子我们的内存池就发挥作用了!
下面我们来重点讲一讲如果自由链表对应的位置没有所需的内存块该怎么办,也就是Refill函数的实现。
static void* Allocate(size_t n)
{
//...
if (ret == NULL)
{
void* r = Refill(RoundUP(n));//没有可用free list 准备装填
}
//...
}
//freelist没有可用区块,将要填充,此时新的空间取自内存池
static void* Refill(size_t n)
{
size_t nobjs = 20;
char* chunk = (char*)ChunkAlloc(n, nobjs); //默认获得20的新节点,但是也可能小于20,可能会改变nobjs
if (nobjs == 1) //如果只有一块直接返回调用者,此时freelist无结点
return chunk;
//有多块,返回一块给调用者,其他挂在自由链表中
Obj* ret = (Obj*)chunk;
Obj* cur = (Obj*)(chunk + n);
Obj* next = cur;
Obj* volatile *myFreeList = FreeList + FreeListIndex(n);
*myFreeList = cur;
for (size_t i = 1; i < nobjs; ++i)
{
next = (Obj*)((char*)cur + n);
cur->freeListLink = next;
cur = next;
}
cur->freeListLink = NULL;
return ret;
}
这里面的重点函数为ChunkAlloc,它的逻辑相对复杂,代码如下:
static char* ChunkAlloc(size_t size, size_t& nobjs)
{
size_t bytesLeft = endFree - startFree; //内存池剩余空间
size_t totalBytes = size * nobjs;
char* ret = NULL;
if (bytesLeft >= totalBytes) // 内存池大小足够分配nobjs个对象大小
{
ret = startFree;
startFree += totalBytes;
return ret;
}
else if (bytesLeft >= size) // 内存池大小不够分配nobjs,但是至少分配一个
{
size_t nobjs = bytesLeft / size;
totalBytes = size * nobjs;
ret = startFree;
startFree += totalBytes;
return ret;
}
else // 内存池一个都分配不了
{
//让内存池剩余的那么点挂在freelist上
if (bytesLeft > 0)
{
size_t index = FreeListIndex(bytesLeft);
((Obj*)startFree)->freeListLink = FreeList[index];
FreeList[index] = (Obj*)startFree;
}
size_t bytesToGet = 2 * totalBytes + RoundUP(heapSize >> 4);
startFree = (char*)malloc(bytesToGet);
if (startFree == NULL)
{
//申请失败,此时试着在自由链表中找
for (size_t i = size; i <= __MAX_BYTES; i += __ALIGN)
{
size_t index = FreeListIndex(i);
Obj* volatile* myFreeList = FreeList + index;
Obj* p = *myFreeList;
if (FreeList[index] != NULL)
{
FreeList[index] = p->freeListLink;
startFree = (char*)p;
endFree = startFree + i;
return ChunkAlloc(size, nobjs);
}
}
endFree = NULL;
//试着调用一级空间配置器
startFree = (char*)MallocAlloc::Allocate(bytesToGet);
}
heapSize += bytesToGet;
endFree = startFree + bytesToGet;
return ChunkAlloc(size, nobjs);
}
}
如果用户需要是一块n字节的区块,且n <= 128(调用第二级配置器),此时Refill填充是这样的:(需要注意的是:系统会自动将n字节扩展到8的倍数也就是RoundUP(n),再将RoundUP(n)传给Refill)。用户需要n块,且自由链表中没有,因此系统会向内存池申请nobjs * n大小的内存块,默认nobjs=20
这就是ChunkAlloc所执行的操作,在执行完ChunkAlloc函数后会获得内存(失败就抛出异常),此时也就是这段代码:
if (nobjs == 1) //如果只有一块直接返回调用者,此时freelist无结点
return chunk;
//有多块,返回一块给调用者,其他挂在自由链表中
Obj* ret = (Obj*)chunk;
Obj* cur = (Obj*)(chunk + n);
Obj* next = cur;
Obj* volatile *myFreeList = FreeList + FreeListIndex(n);
*myFreeList = cur;
for (size_t i = 1; i < nobjs; ++i)
{
next = (Obj*)((char*)cur + n);
cur->freeListLink = next;
cur = next;
}
cur->freeListLink = NULL;
如果只有一块返回给调用者,有多块,返回给调用者一块,剩下的挂在对应的位置。
这样一个空间配置的比较关键思路就有了,剩余的可以参看stl源码剖析。
关于完整代码可见 我的github
也就是STL可能存在的问题,通俗的讲就是优缺点吧
我们知道,引入相对的复杂的空间配置器,主要源自两点:
1. 频繁使用malloc,free开辟释放小块内存带来的性能效率的低下
2. 内存碎片问题,导致不连续内存不可用的浪费
引入两层配置器帮我们解决以上的问题,但是也带来一些问题: