SGI内存池详解
SGI空间配置器,分为一级配置器和二级配置器,一级配置器对malloc和free(new和delete)做了一次封装,并且开通了预申请的接口。二级配置器,是真正的内存池,减少了内存开辟和释放所进行的上下文切换,提高了内存申请和释放效率。
话不多说,边看代码,边解释。
一级空间配置器
从一级配置器开始,我们顺着思路来,看到哪里说到哪里,先看一下,1号成员
private:
//函数指针成员,对,他就是释放预申请内存的接口
static void (*__malloc_alloc_oom_headler)();
typedef void (*OOM_Headler)(); //类型函数指针重命名
看一下他的两个私有方法,oom_malloc、oom_realloc,当我们malloc和realloc堆内存空间不足时,我们释放预申请的内存空间再次申请,如果还申请不到那就只能一首凉凉送给你。
private:
static void* oom_malloc(size_t n)
{
void* result = nullptr;
void (*my_malloc_handler)() = nullptr;
for (;;)
{
my_malloc_handler = __malloc_alloc_oom_headler;
if (nullptr == my_malloc_handler)
{
__THROW_BAD_ALLOC;
//打印日志,并退出的宏
//#define __THROW_BAD_ALLOC \
// std::cerr << "out of memory" << std::endl; \
// exit(1)
}
//my_malloc_handler 意义
//1. 释放更多的空间
//2. 指明新的释放函数
//3.没有释放函数也要指明一个结束函数
(*my_malloc_handler)();
result = malloc(n);
if (nullptr != result)
{
return result;
}
}
}
static void* oom_realloc(void* p, size_t new_sz)
{
void* result = nullptr;
void (*my_malloc_handler)() = nullptr;
for (;;)
{
my_malloc_handler = __malloc_alloc_oom_headler;
if (nullptr == my_malloc_handler)
{
__THROW_BAD_ALLOC;
}
(*my_malloc_handler)();
result = realloc(p, new_sz);
if (nullptr != result)
{
return result;
}
}
}
看一下,他对外部所提供的接口,本质是对malloc、realloc进行了封装
allocate,如果malloc 、realloc申请不到内存,调用oom_malloc 进行释放预留空间,再次内存申请
public:
static void* allocate(size_t n)
{
void* result = malloc(n);
if (nullptr == result)
{
result = oom_malloc(n);
}
return result;
}
static void* reallocate(void* p, size_t old_sz, size_t new_sz)
{
void* result = realloc(p, new_sz);
if (nullptr == result)
{
result = oom_realloc(p, new_sz);
}
return result;
}
deallocate,就是free
static void deallocate(void* p)
{
free(p);
}
set_malloc_headller,设置释放预留空间的接口,设置新的返回旧的,我们进行一级配置器演示的Demo的时候,我们就明白他在干什么啦。
public:
static OOM_Headler set_malloc_headller(OOM_Headler f)
{
OOM_Headler old = __malloc_alloc_oom_headler;
__malloc_alloc_oom_headler = f;
return old;
}
一级空间配置器的Demo
//预申请的两个空间
char* p1 = (char*)malloc(sizeof(char) * 1024 * 1024 * 100);
char* p2 = (char*)malloc(sizeof(char) * 1024 * 1024 * 200);
//release1 和 release2 就是两个释放预审空间的函数,他需要具备一下功能
//1. 释放更多的空间
//2. 指明新的释放函数
//3.没有释放函数也要指明一个结束函数
void release2()
{
free(p2);
p2 = nullptr;
//2号 设置3号 or 结束函数
peter::malloc_alloc::set_malloc_headller(nullptr);
}
void release1()
{
free(p1);
p1 = nullptr;
//1号 设置2号 or 结束函数
peter::malloc_alloc::set_malloc_headller(release2);
}
int main()
{
//首先注册1号 释放函数接口
peter::malloc_alloc::set_malloc_headller(release1);
int* p = (int*)peter::malloc_alloc::allocate(sizeof(int));
*p = 100;
peter::malloc_alloc::deallocate(p);
p = nullptr;
return 0;
}
二级空间配置器
看了看一级空间配置级,瞬间感觉还是很简单的呀,那么我们再来看看二级配置器。我觉得直接看代码,可能我们不好理解,我们先简单阐述一下,他的原理。在你申请的内存大于128个字节的时候呢,会直接调用一级配置器,当申 请的内存小于128个内存时候,他将其划分为8,16,24,32…128, 15个不同的等级,每个等级下面都链接这大小相等的内存块。
抽象图,就是这样,比如你想申请,29个字节大小的内存块,内存池就从头部取一个32字节大小的内存块给你,你用完之后又还回来。大概有个了解,我们一起来看代码
//类型重命名了一级配置器
typedef __malloc_alloc_template<0> malloc_alloc;
//设置单位大小为8个字节
enum
{
__ALIGN = 8
};
//最大单位块为128
enum
{
__MAX_BYTES = 128
};
enum
{
//计算出一共又多少块
__NFREELIST = __MAX_BYTES / __ALIGN
};
先看他的私有成员
private:
union obj //可以理解为链表的node类型
{
union obj* free_list_link; // next指针
char client_data[1];
};
static obj* volatile free_list[__NFREELIST]; // 上图的表头
static char* start_free; //在池子中,但是还没有挂载表上的内存开始位置
static char* end_free; //在池子中,但是还没有挂载表上的内存结束位置
static size_t heap_size; //内存池现在,总计内存大小
两个hash函数
//他在将 1-8 --> 8
// 9-16-->16
// 17-24-->24
static size_t ROUND_UP(size_t bytes)
{
return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);
}
//他在将 1-8 --> 8 所对应的下标 0
// 9-16-->16 所对应的下标 1
// 17-24-->24 所对应的下标 2
// ..........................
static size_t FREELIST_INDEX(size_t bytes)
{
return (bytes + __ALIGN - 1) / __ALIGN - 1;
}
下来当然是申请,重新申请,释放函数啦,我们与一个一个欣赏
allocate,对应malloc
public:
static void* allocate(size_t n)
{
if (n > __MAX_BYTES) //大于128,一级配置器见
{
return malloc_alloc::allocate(n);
}
obj* volatile* my_free_list = nullptr;
obj* result = nullptr;
my_free_list = free_list + FREELIST_INDEX(n); //定位到内存链位置
result = *my_free_list; //获取内存链的首位
if (nullptr == result) //先不考虑,假装他有
{
void* r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result->free_list_link; //链表指向下一个
return result; //返回了链表头部内存
}
deallocate,对应free
static void deallocate(void* p, size_t n)
{
if (n > (size_t)__MAX_BYTES) //大于128 一级配置器你的活
{
return malloc_alloc::deallocate(p);
}
obj* q = (obj*)p;//为什么需要强转成obj* 类型,因为有他,才有next指针
obj* volatile* my_free_list = free_list + FREELIST_INDEX(n);
//链表头插法,大家不陌生吧,把free_list_link看成next
q->free_list_link = *my_free_list;
*my_free_list = q;
}
reallocate,对应realloc
static void* reallocate(void* p, size_t old_sz, size_t new_sz)
{
//一级配置器的活
if (old_sz > __MAX_BYTES && new_sz > __MAX_BYTES)
{
return malloc_alloc::reallocate(p, old_sz, new_sz);
}
//举个例子 原来是 7 现在要申请 8 ,我原来就给了你8字节大小,不用折腾了还是这块
if (ROUND_UP(old_sz) == ROUND_UP(new_sz))
{
return p;
}
//sz 获取新旧最小的大小
size_t sz = old_sz > new_sz ? new_sz : old_sz;
//申请新的内存块
void* q = allocate(new_sz);
//以最小为基准,移动内容
memmove(q, p, sz);
//返还旧的空间到内存池
deallocate(p, old_sz);
return q;
}
有没有感受到效率倍增。
下来我们看一下refill、chunk_alloc这两个私有函数,他们是干什么的呢?
1、refill:完成链表的创建
2、chunk_alloc:向池子中注入水(内存)
先看refill,问题:
1、我要申请20个8字节大小的内存块,申请不到怎么办
2、如果申请不到20个,能申请多少个
static void* refill(size_t n)
{
int nobjs = 20; //如果链表中没有内存块了,默认我直接给他申请20个
//chunk_alloc 的 nobjs是一个引用传递,标志这最终能够申请多少块
char* chunk = chunk_alloc(n, nobjs);
//只能申请一块,返回就好了
if (1 == nobjs)
{
return chunk;
}
//返回多个呢,我将从第二个内存块开始,全部链接起来挂在表上,n表示内存块的大小
//没有理解的话,看下图吧
for (int i = 1; i < nobjs - 1; i++)
{
obj* q = (obj*)(chunk + n * i);
q->free_list_link = (obj*)(chunk + n * (i + 1));
}
obj* q = (obj*)(chunk + n * (nobjs - 1));
q->free_list_link = nullptr;
obj* volatile* my_free_list = free_list + FREELIST_INDEX(n);
*my_free_list = (obj*)(chunk + n);
//返回第一个
return chunk;
}
chunk_alloc,向池子中注水,问题
1、怎么注,什么时候注、注多少,
2、申请不到水怎么办
如果没有看懂注释,结合这图一起看吧
static char* chunk_alloc(size_t size, int& nobjs)
{
char* result = nullptr;
size_t total_bytes = size * nobjs;//需求注入
size_t bytes_left = end_free - start_free; //现存水量
if (bytes_left >= total_bytes) //有水
{
result = start_free;
start_free = start_free + total_bytes; //start_free 下降
return result; //返回内存,交给refill,组织链接
}
else if (bytes_left > size) //水不足,但是至少够一个 如图 情况二
{
nobjs = bytes_left / size; //实际能满足多少个
total_bytes = size * nobjs; //实际大小
result = start_free;
start_free = start_free + total_bytes; //start_free 下降
return result; //返回内存,交给refill,组织链接
}
else //没水,或者水连一个都不够
{
//申请水,需求的2倍 外加一个偏移量
size_t bytes_to_get = total_bytes * 2 + ROUND_UP(heap_size >> 4);
if (bytes_left > 0) //还有点水,挂到对应的表下,比如剩下24 就挂在2号表格下
{
obj* volatile* my_free_list = free_list +
(FREELIST_INDEX(bytes_left));
((obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = ((obj*)start_free);
}
//准备注水,标记归位
start_free = end_free = nullptr;
// start_free = (char *)malloc(bytes_to_get);
//调用一级配置器申请,问题 申请不到怎么办 参考如同情况三
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
if (nullptr == start_free) //申请不到
{
obj* volatile* my_free_list = nullptr;
obj* p = *my_free_list;
//遍历所有比他大的表头区域,如果存在,取一块出来,先用用(这里取出来就不会还了)
for (int i = size; i < __MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
obj* p = *my_free_list;
if (p != nullptr)
{
*my_free_list = p->free_list_link;
start_free = (char*)p;
end_free = start_free + i;
//借到一块,重新看一下吧,有水了
//真走到这里,有点危险了
return chunk_alloc(size, nobjs);
}
}
}
//一级配置器 申请到水了,全部注入
end_free = start_free + bytes_to_get;
//池子大小增加
heap_size += bytes_to_get;
//重新探测,已经注入水源
return chunk_alloc(size, nobjs);
}
}
情况一,从无到有,分配完成之后还有剩余
情况二,至少够一个
情况三,不足一个,且没有一级配置器无法申请到内存
二级空间配置器Demo
通过Debug,我们可以看到其中所有过程
int main()
{
int n = 15;
int tag;
while (cin >> tag, tag != -1)
{
char* p = (char*)peter::alloc::allocate(n);
}
n = 24;
while (cin >> tag, tag != -1)
{
char* p = (char*)peter::alloc::allocate(n);
}
return 0;
}
参考文献
·《STL源码剖析》 侯捷