咬碎STL空间配置器

时间:2023-03-09 08:58:03
咬碎STL空间配置器

STL空间配置器

一、开场白:

给我的感觉就是,了解是空间配置器的功能,是那么的明了;在看原理,我还是很开心;接下来是360度大转变:

那么长的变量或者函数命名、那么多的宏、不爽,不过,遇上我这种二货,是精华,我也给嚼碎了,下面开始吧:

二、STL是什么:

1.STL(Standard TemplateLibrary),即标准模板库,是一个具有工业强度的,高效的C++程序库。

2.它被容纳于C++标准程序库(C++ StandardLibrary)中,是ANSI/ISO C++标准中最新的也是极具革命性的一部分。

3.该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。

4.STL(Standard TemplateLibrary,标准模板库),从根本上说,STL是一些“容器”的集合,这些“容器”有list,vector,set,map等,STL也是算法和其他一些组件的集合。

一句话:就是为了有品味的偷懒,设计出来造福广大码农的。

三、STL空间配置器:

1>>为什么要用空间配置器:

在软件开发,使用很多的小块内存,在程序中动态申请,释放。

那么问题就来了:

1:内存碎片问题。(这里是外碎片问题)

2:频繁的小块内存申请,调用malloc,系统调用产生性能问题。

另外说明:

1.内碎片:因为内存对齐、访问效率而产生 。如 用户需要6字节,实际得到8或者10字节的问题,其中的碎片是浪费掉的。

2.外碎片:系统中内存总量足够,但是不连续,所以无法分配给用户使用而产生的浪费。如下图所示:

咬碎STL空间配置器

频繁分配和 释放过后,空白区域不在连续,当程序需要再次分配没存时,虽然零碎的内存加起来远远大过程序需要申请的内存,但是,内存申请,只认连续的,那么不连续,大多就荒废了!

2>>怎么用:

知道了问题,那么空间配置器就是来解决问题的,那么如何解决呢:下面来看看:

在stl_alloc.h中定义了两级配置器,主要思想是申请大块内存池,小块内存直接从内存池中申请,当不够用时再申请新的内存池,还有就是大块内存直接申请。当申请空间大于128字节时调用第一级配置器,第一级配置器没有用operator::new和operator::delete来申请空间,而是直接调用malloc/free和realloc,并且实现了类似c++中new-handler的机制。所谓c++
new handler机制是,你可以要求系统在内存配置需求无法被满足时,调用一个指定的函数。换句话说,一旦::operator::new无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客端指定的处理例程,该处理例程通常称为new-handler.new-handler解决内存做法有特定的模式
。SGI第一级配置器的allocate()和realloc都是在调用malloc和realloc不成功后,改调用oom_malloc()和oom_realloc(),后两者都有内循环,不断调用"内存不足处理例程",期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程“并未被客端设定,oom_malloc()和oom_realloc便调用_THROW_BAD_ALLOC,
丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序。

     在stl_alloc.h中定义的第二级配置器中,如果区块够大,超过128字节时,就移交给第一级配置器处理。当区块小于128字节时,则以内存池管理,此法又称为次层配置,每次配置一大块内存,并维护对应的*链表(free-list)。下次若再有相同大小的内存需求,就直接从free-list中拔出,关于*链表:

咬碎STL空间配置器

如果客端释还小额区块,就由配置器回收到free-lists中,另外,配置器除了负责配置,也负责回收。为了管理方便,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数。并维护16个free-lists,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,
112,120,128 字节的小额区块。当申请小于等于128字节时就会检查对应的free list,如果free-list中有可用的区块,就直接拿来,如果没有,就准备为对应的free-list 重新填充空间。新的空间将取自内存池,缺省取得20个新节点,如果内存池不足(但是还足以配置一个以上的节点),就返回的相应的节点数.如果当内存池中连一个节点大小都不够时,就申请新的内存池,大小为2*total_bytes+ROUND_UP(heap_size>>4),totoal_bytes 为申请的空间大小,ROUND_UP调整为8的倍数,heap_size为当前总申请内存池的大小。如果申请该内存池成功就把原来内存池中剩下的空间分配给适当的free-list.万一山穷水尽,整个system
heap空间都不够了(以至无法为内存池注入源头活水),malloc()行动失败,就会四处寻找有无"尚有未用区块,且区块足够大 "之free lists.找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器其实也是使用malloc来配置内存。但它有out-of-memory处理机制(类似new-handler机制),或许有机会释放其他的内存拿来此处使用。如果可以就成功,否则发出bad_alloc异常。

实现时,allocator需要维护一个存储16个空闲块列表表头的数组free_list,数组元素i是一个指向块大小为8*(i+1)字节的空闲块列表的表头,一个指向内存池起始地址的指针start_free和一个指向结束地址的指针end_free。空闲块列表节点的结构如下:

union obj
{
union obj * free_list_link;
char client_data[1];
};

这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的时用户的数据。因此,allocator中的空闲块链表可以表示成:

    obj* free_list[16];

下面看看一个博友的伪代码,写的相当不错,值得借鉴:

// 算法:allocate
// 输入:申请内存的大小size
// 输出:若分配成功,则返回一个内存的地址,否则返回NULL
{
if(size 大于 128)
启动第一级分配器直接调用malloc分配所需的内存并返回内存地址;
else
{
将size向上round up成8的倍数并根据大小从free_list中取对应的表头free_list_head
if(free_list_head 不为空)
{
从该列表中取下第一个空闲块并调整free_list,返回free_list_head
}
else
{
调用refill算法建立空闲块列表并返回所需的内存地址
}
}
} // 算法:refill
// 输入:内存块的大小size
// 输出:建立空闲块链表并返回第一个可用的内存地址
{
调用chunk_alloc算法分配若干个大小为size的连续内存区域并返回起始地址chunk和成功分配的块数nobj
if(块数为1)
直接返回 chunk;
else
{
开始在chunk地址块中建立free_list
根据size取free_list中对应的表头元素free_list_head
将free_list_head 指向chunk中偏移起始地址为size的地址处,即free_list_head = (obj*)(chunk+size)
再将整个chunk中剩下的nobj-1个内存块串联起来构成一个空闲列表
返回chunk,即chunk中第一个空闲的内存块
}
} // 算法:chunk_alloc
// 输入:内存块的大小size,预分配的内存块数nobj(以引用传递)
// 输出:一块连续的内存区域的地址和该区域内可以容纳的内存块的块数
{
计算总共所需的内存大小total_bytes
if(内存池足以分配,即end_free-start_free >= total_bytes)
{
则更新start_free
返回旧的start_free
}
else if(内存池不够分配nobj个内存块,但至少可以分配一个)
{
计算可以分配的内存块数并修改nobj
更新start_free并返回原来的start_free
}
else // 内存池连一个内存块都分配不了
{
先将内存池的内存块链入到对应的free_list中后
调用malloc操作重新分配内存池,大小为2倍的total_bytes为附加量,start_free指向返回的内存地址
if(分配不成功)
{
if(16个空闲列表中尚有空闲块)
尝试将16个空闲列表中空闲块回收到内存池中再调用chunk_alloc(size,nobj)
else
调用第一级分配器尝试out of memory机制是否还有用
}
更新end_free为start_free+total_bytes,heap_size为2倍的total_bytes
调用chunk_alloc(size,nobj)
}
} // 算法:deallocate
// 输入:需要释放的内存块地址p和大小size
{
if(size 大于128字节)
直接调用free(p)释放
else
{
将size向上取8的倍数,并据此获取对应的空闲列表表头指针free_list_head
调整free_list_head将p链入空闲列表块中
}
}

以下为一级和二级空间配置器源码:

allocator.h

#pragma once

#include <stdio.h>
#include <stdarg.h> #define __DEBUG__ static string GetFileName(const string& path)
{
char ch = '/'; #ifdef _WIN32
ch = '\\';
#endif size_t pos = path.rfind(ch);
if (pos == string::npos)
{
return path;
}
else
{
return path.substr(pos + 1);
}
}
// 用于调试追溯的trace log
inline static void __trace_debug(const char* function,
const char* filename, int line, char* format, ...)
{
#ifdef __DEBUG__
// 输出调用函数的信息
fprintf(stdout, "【%s:%d】-%s", GetFileName(filename).c_str(), line, function); // 输出用户打的trace信息
va_list args;
va_start (args, format);
vfprintf (stdout, format, args);
va_end (args);
#endif
} #define __TRACE_DEBUG(...) \
__trace_debug(__FUNCTION__, __FILE__, __LINE__, __VA_ARGS__); ////////////////////////////////////////////////////////////////////////////
// 以下是模拟实现SGI STL30版的内存配置器。 // SimpleAlloc统一封装的内存分配的接口
template<class T, class Alloc>
class SimpleAlloc
{
public:
static T *Allocate(size_t n)
{
return 0 == n? 0 : (T*) Alloc::Allocate(n * sizeof (T));
} static T *Allocate(void)
{
return (T*) Alloc::Allocate(sizeof (T));
} static void Deallocate(T *p, size_t n)
{
if (0 != n) Alloc::Deallocate(p, n * sizeof (T));
} static void Deallocate(T *p)
{
Alloc::Deallocate(p, sizeof (T));
}
}; ///////////////////////////////////////////////////////////////////////////
// 一级空间配置器(malloc/realloc/free)
// // 内存分配失败以后处理的句柄handler类型
typedef void(*ALLOC_OOM_FUN)();
template <int inst>
class __MallocAllocTemplate
{
private:
//static void (* __sMallocAllocOomHandler)();
static ALLOC_OOM_FUN __sMallocAllocOomHandler; static void * OomMalloc(size_t n)
{
ALLOC_OOM_FUN handler;
void* result; //
// 1:分配内存成功,则直接返回
// 2:若分配失败,则检查是否设置处理的handler,
// 有则调用以后再分配。不断重复这个过程,直到分配成功为止。
// 没有设置处理的handler,则直接结束程序。
//
for (;;) {
handler = __sMallocAllocOomHandler;
if (0 == handler)
{
cerr<<"out of memory"<<endl;
exit(-1);
} handler(); result = malloc(n);
if (result)
return(result);
}
} static void *OomRealloc(void* p, size_t n)
{
// 同上
ALLOC_OOM_FUN handler;
void* result; for (;;) {
handler = __sMallocAllocOomHandler;
if (0 == handler)
{
cerr<<"out of memory"<<endl;
exit(-1);
} (*handler)();
result = realloc(p, n);
if (result) return(result);
}
}
public:
static void * Allocate(size_t n)
{
__TRACE_DEBUG("(n:%u)\n", n); void *result = malloc(n);
if (0 == result) result = OomMalloc(n);
return result;
} static void Deallocate(void *p, size_t /* n */)
{
__TRACE_DEBUG("(p:%p)\n", p); free(p);
} static void* Reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz);
if (0 == result) result = OomRealloc(p, new_sz);
return result;
} static void (* SetMallocHandler(void (*f)()))()
{
void (* old)() = __sMallocAllocOomHandler;
__sMallocAllocOomHandler = f;
return(old);
}
}; // 分配内存失败处理函数的句柄函数指针
template <int inst>
ALLOC_OOM_FUN __MallocAllocTemplate<inst>::__sMallocAllocOomHandler = 0; typedef __MallocAllocTemplate<0> MallocAlloc; //#define __USE_MALLOC # ifdef __USE_MALLOC
typedef __MallocAllocTemplate<0> MallocAlloc;
typedef MallocAlloc Alloc;
# else ////////////////////////////////////////////////////////////////////////
// 二级空间配置器 template <bool threads, int inst>
class __DefaultAllocTemplate
{
public:
enum {__ALIGN = 8}; // 排列基准值(也是排列间隔)
enum {__MAX_BYTES = 128}; // 最大值
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; // 排列链大小 static size_t ROUND_UP(size_t bytes)
{
// 对齐
return ((bytes + __ALIGN - 1) & ~(__ALIGN - 1));
} static size_t FREELIST_INDEX(size_t bytes)
{
// bytes == 9
// bytes == 8
// bytes == 7
return ((bytes + __ALIGN - 1)/__ALIGN - 1);
} union Obj
{
union Obj* _freeListLink; // 指向下一个内存块的指针
char _clientData[1]; /* The client sees this.*/
}; static Obj* volatile _freeList[__NFREELISTS]; // *链表
static char* _startFree; // 内存池水位线开始
static char* _endFree; // 内存池水位线结束
static size_t _heapSize; // 从系统堆分配的总大小 // 获取大块内存插入到*链表中
static void* Refill(size_t n);
// 从内存池中分配大块内存
static char* ChunkAlloc(size_t size, int &nobjs); static void * Allocate(size_t n);
static void Deallocate(void *p, size_t n);
static void* Reallocate(void *p, size_t old_sz, size_t new_sz);
}; // 初始化全局静态对象
template <bool threads, int inst>
typename __DefaultAllocTemplate<threads, inst>::Obj* volatile __DefaultAllocTemplate<threads,inst>::_freeList[__DefaultAllocTemplate<threads, inst>::__NFREELISTS]; 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>
void* __DefaultAllocTemplate<threads, inst>::Refill(size_t n)
{
__TRACE_DEBUG("(n:%u)\n", n); //
// 分配20个n bytes的内存
// 如果不够则能分配多少分配多少
//
int nobjs = 20;
char* chunk = ChunkAlloc(n, nobjs); // 如果只分配到一块,则直接这块内存。
if(nobjs == 1)
return chunk; Obj* result, *cur;
size_t index = FREELIST_INDEX(n);
result = (Obj*)chunk; // 把剩余的块链接到*链表上面
cur = (Obj*)(chunk+n);
_freeList[index] = cur;
for(int i = 2; i < nobjs; ++i)
{
cur->_freeListLink = (Obj*)(chunk+n*i);
cur = cur->_freeListLink;
} cur->_freeListLink = NULL;
return result;
} template <bool threads, int inst>
char* __DefaultAllocTemplate<threads, inst>::ChunkAlloc(size_t size, int &nobjs)
{
__TRACE_DEBUG("(size: %u, nobjs: %d)\n", size, nobjs); char* result;
size_t bytesNeed = size*nobjs;
size_t bytesLeft = _endFree - _startFree; //
// 1.内存池中的内存足够,bytesLeft>=bytesNeed,则直接从内存池中取。
// 2.内存池中的内存不足,但是够一个bytesLeft >= size,则直接取能够取出来。
// 3.内存池中的内存不足,则从系统堆分配大块内存到内存池中。
//
if (bytesLeft >= bytesNeed)
{
__TRACE_DEBUG("内存池中内存足够分配%d个对象\n", nobjs); result = _startFree;
_startFree += bytesNeed;
}
else if (bytesLeft >= size)
{
__TRACE_DEBUG("内存池中内存不够分配%d个对象,只能分配%d个对象\n", nobjs, bytesLeft / size);
result = _startFree;
nobjs = bytesLeft / size;
_startFree += nobjs*size;
}
else
{
// 若内存池中还有小块剩余内存,则将它头插到合适的*链表
if (bytesLeft > 0)
{
size_t index = FREELIST_INDEX(bytesLeft);
((Obj*)_startFree)->_freeListLink = _freeList[index];
_freeList[index] = (Obj*)_startFree;
_startFree = NULL; __TRACE_DEBUG("将内存池中剩余的空间,分配给freeList[%d]\n", index);
} // 从系统堆分配两倍+已分配的heapSize/8的内存到内存池中
size_t bytesToGet = 2*bytesNeed + ROUND_UP(_heapSize>>4);
_startFree = (char*)malloc(bytesToGet);
__TRACE_DEBUG("内存池空间不足,系统堆分配%u bytes内存\n", bytesToGet); //
// 【无奈之举】
// 如果在系统堆中内存分配失败,则尝试到*链表中更大的节点中分配
//
if (_startFree == NULL)
{
__TRACE_DEBUG("系统堆已无足够,无奈之下,智能到*链表中看看\n"); for(int i = size; i <= __MAX_BYTES; i+=__ALIGN)
{
Obj* head = _freeList[FREELIST_INDEX(size)];
if (head)
{
_startFree = (char*)head;
_freeList[FREELIST_INDEX(size)] = head->_freeListLink;
_endFree = _startFree+i;
return ChunkAlloc(size, nobjs);
}
} //
// 【最后一根稻草】
// *链表中也没有分配到内存,则再到一级配置器中分配内存,
// 一级配置器中可能有设置的处理内存,或许能分配到内存。
//
__TRACE_DEBUG("系统堆和*链表都已无内存,一级配置器做最后一根稻草\n");
_startFree = (char*)MallocAlloc::Allocate(bytesToGet);
} // 从系统堆分配的总字节数。(可用于下次分配时进行调节)
_heapSize += bytesToGet;
_endFree = _startFree + bytesToGet; // 递归调用获取内存
return ChunkAlloc(size, nobjs);
} return result;
} template <bool threads, int inst>
void* __DefaultAllocTemplate<threads, inst>::Allocate(size_t n)
{
__TRACE_DEBUG("(n: %u)\n", n); //
// 若 n > __MAX_BYTES则直接在一级配置器中获取
// 否则在二级配置器中获取
//
if (n > __MAX_BYTES)
{
return MallocAlloc::Allocate(n);
} size_t index = FREELIST_INDEX(n);
void* ret = NULL; //
// 1.如果*链表中没有内存则通过Refill进行填充
// 2.如果*链表中有则直接返回一个节点块内存
// ps:多线程环境需要考虑加锁
//
Obj* head = _freeList[index];
if (head == NULL)
{
return Refill(ROUND_UP(n));
}
else
{
__TRACE_DEBUG("*链表取内存:_freeList[%d]\n", index); _freeList[index] = head->_freeListLink;
return head;
}
} template <bool threads, int inst>
void __DefaultAllocTemplate<threads, inst>::Deallocate(void *p, size_t n)
{
__TRACE_DEBUG("(p:%p, n: %u)\n",p, n); //
// 若 n > __MAX_BYTES则直接归还给一级配置器
// 否则在放回二级配置器的*链表
//
if (n > __MAX_BYTES)
{
MallocAlloc::Deallocate(p, n);
}
else
{
// ps:多线程环境需要考虑加锁
size_t index = FREELIST_INDEX(n); // 头插回*链表
Obj* tmp = (Obj*)p;
tmp->_freeListLink = _freeList[index];
_freeList[index] = tmp;
}
} template <bool threads, int inst>
void* __DefaultAllocTemplate<threads, inst>::Reallocate(void *p, size_t old_sz, size_t new_sz)
{
void * result;
size_t copy_sz; if (old_sz > (size_t) __MAX_BYTES && new_sz > (size_t) __MAX_BYTES) {
return(realloc(p, new_sz));
}
if (ROUND_UP(old_sz) == ROUND_UP(new_sz))
return p; result = Allocate(new_sz);
copy_sz = new_sz > old_sz? old_sz : new_sz;
memcpy(result, p, copy_sz);
Deallocate(p, old_sz);
return result;
} typedef __DefaultAllocTemplate<false, 0> Alloc;
#endif // __USE_MALLOC // 通过__TRACE_DEBUG做白盒测试 // 测试内存池的一级、二级配置器功能
void Test1()
{
// 测试调用一级配置器分配内存
cout<<"测试调用一级配置器分配内存"<<endl;
char*p1 = SimpleAlloc<char, Alloc>::Allocate(129);
SimpleAlloc<char, Alloc>::Deallocate(p1, 129); // 测试调用二级配置器分配内存
cout<<"测试调用二级配置器分配内存"<<endl;
char*p2 = SimpleAlloc<char, Alloc>::Allocate(128);
char*p3 = SimpleAlloc<char, Alloc>::Allocate(128);
char*p4 = SimpleAlloc<char, Alloc>::Allocate(128);
char*p5 = SimpleAlloc<char, Alloc>::Allocate(128);
SimpleAlloc<char, Alloc>::Deallocate(p2, 128);
SimpleAlloc<char, Alloc>::Deallocate(p3, 128);
SimpleAlloc<char, Alloc>::Deallocate(p4, 128);
SimpleAlloc<char, Alloc>::Deallocate(p5, 128); for (int i = 0; i < 21; ++i)
{
printf("测试第%d次分配\n", i+1);
char*p = SimpleAlloc<char, Alloc>::Allocate(128);
}
} // 测试特殊场景
void Test2()
{
cout<<"测试内存池空间不足分配20个"<<endl;
// 8*20->8*2->320
char*p1 = SimpleAlloc<char, Alloc>::Allocate(8); char*p2 = SimpleAlloc<char, Alloc>::Allocate(8); cout<<"测试内存池空间不足,系统堆进行分配"<<endl;
char*p3 = SimpleAlloc<char, Alloc>::Allocate(12);
} // 测试系统堆内存耗尽的场景
void Test3()
{
cout<<"测试系统堆内存耗尽"<<endl; SimpleAlloc<char, Alloc>::Allocate(1024*1024*1024);
//SimpleAlloc<char, Alloc>::Allocate(1024*1024*1024);
SimpleAlloc<char, Alloc>::Allocate(1024*1024); // 不好测试,说明系统管理小块内存的能力还是很强的。
for (int i = 0; i < 100000; ++i)
{
char*p1 = SimpleAlloc<char, Alloc>::Allocate(128);
}
}

四、讨论一个问题:

对于内存池来说,这样的大内存池子是用malloc来的,想释放,当然用free好了,但是,对于从内存池中跑向*链表之下的小块内存,如何释放?

其实*链表中的内存既内有还给操作系统,也没有还给内存池,在*链表中,且配置器的所有方法,成员都是静态的,那么他们就是存放在静态区。这些内存即在程序结束释放。