【STL】SGI版STL空间配置器剖析+简单实现

简单认识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个所需块的总大小,如果内存池剩余的不够这么多,那如果至少能分配一个所需大小的空间它也返回。如果一个都不够的话,它会从自由链表里寻找是否还有没用的区块,找到了就拿出来,找不到就调用一级空间配置器。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值