简单内存池的设计

- 内存池的设计思想,每次获取空间时,先看内存池中有没有自由内存,如果有就把自由内存返回,如果没有,就在一次性向堆中malloc一大块内存,分割成很多的小块,返回第一个小块,剩下的作为内存池备用。这样即减少了malloc的次数,也减少的因为cookie的浪费。
class myAllocator {
private:
struct obj{
obj* next;
};
public:
void* allocate(size_t size);
void deallocate(void* ptr);
private:
obj* freestore = nullptr;
const int CHUNK = 10;
};
void* myAllocator::allocate (size_t size) {
obj* p;
if(!freestore) {
size_t chunk = size * CHUNK;
p = (obj*)malloc(chunk);
freestore = p;
for(int i = 1;i <= CHUNK-1; ++i) {
p->next = (obj*)((size_t)p + size);
p = p->next;
}
p->next = nullptr;
}
p = freestore;
freestore = p->next;
return p;
}
void myAllocator::deallocate(void* ptr) {
((obj*)ptr)->next = freestore;
freestore = (obj*)ptr;
}
class Foo {
public:
long L;
Foo(long x = 1) : L(x) {}
static myAllocator myalloc;
void* operator new(size_t size) {
return myalloc.allocate(size);
}
void operator delete(void* ptr) {
return myalloc.deallocate(ptr);
}
};
myAllocator Foo::myalloc;
STL标准模板库中的alloc
- 在gnuC2.9的版本中使用的容器的分配器就是一个非常经典的内存池设计。在前面说的自定义分配器中是以元素为单位,比如int类型就是分配4字节,double就分配9字节。而在gnuc 2.9中的分配器alloc,则是以字节为单位进行分配的。
- 在分配器里面维护了一条链表,里面装了16个指针,分别对应8字节,16字节,24字节最后到128字节,这些指针指向对应的自由内存,当系统要求分配某个字节,就到对应的指针里去看有没有内存。除了没有指针指向的自由内存,还有一个备用池也会有自由内存。这样吧,我举个具体的申请例子来讲述这个分配器的运行过程。
- 运行过程:
- 首先假如我需要申请32字节的空间,分配器就会先去看链表里面第四个也就是对应32字节的位置下面有没有内存,如果没有,就看备用池有没有,如果也没有,就向系统一次性malloc 20 _ 2 _ 32字节的内存,也就是40块 32字节的内存,但是只使用前20个,返回第一块给客户,然后19块放到刚刚说的链表第四个指针下面,剩下20个放在备用池里面。这样下一次如果在申请32字节大小的内存,就会到链表第四个指针下找刚刚放上去的空间。
- 然后加入我又要申请64字节,刚刚是32字节,现在是64字节,分配器也是会先到对应链表里面第八个指针看上面有没有自由空间,没有,就看备用池里面有没有,备用池里面有刚刚额外申请的20块32字节的空间,这个时候就会把这些空间先分成每块64字节,应该是10块把,返回第一块给客户,剩下9块挂在第八个指针下面。
- 大概的运行过程就是这样,当然还有一些细节,就是刚刚说的每次malloc会额外申请20块内存作为备用池的自由内存,细节就是这个申请还有一个追加量,会根据你累计申请空间量,动态调整追加量,越申请,每次追加量就越多。这其实就和vector容器每次扩容是倍增的原理一样,总共申请的越多说明之后可能还会要求更多。
- 当然还有其他细节,比如备用池最多分出20块对应的内存,还有当备用池不够作为一块内存时会先把这个碎片放到对应的链表上在申请空间等等,当时总之alloc内存池的设计大概是这样,当然我只是学习其中原理,并没有手动实现过这个内存池。最后要说的是,这个优秀的内存池设计背后在gnuC 4.9 被放弃了专用没有任何特殊设计的普通分配器了,我也不知道具体的原因。