空间配置器的作用是在底层为上层的各种容器提供存储空间,需要多少分配多少,一般分配的比你需要的更多。打个不恰当的比喻,空间配置器和容器之间的关系,相当于将军和粮草的关系。当然了,容器相当于将军,它在阵前杀敌,冲锋陷阵,处理各种事情;而空间配置器就相当于粮草,给前阵提供源源不断的供给;如果一个将军想打胜仗,那必须后方粮草充足才行。
为了进一步提高内存的使用率和使用效率。主要是从以下两方面来考虑的:
1.小块内存会带来内存碎片问题
如果任由STL中的容器自行通过malloc分配内存,那么频繁的分配和释放内存会导致堆中有很多的外部碎片。可能堆中的所有空闲空间之和很大,但当申请新的内存的请求到来时,没有足够大的连续内存可以分配,这将导致内存分配失败。因此这样会导致内存浪费。
2.小块内存的频繁申请释放会带来性能问题
开辟空间的时候,分配器需要时间去寻找空闲块,找到空闲块之后才能分配给用户。而如果分配器找不到足够大的空闲块可能还需要考虑处理加碎片现象(释放的小块空间没有合并),这时候需要花时间去合并已经释放了的内存空间块。
而且malloc在开辟内存空间的时候,还会附带附加的额外信息,因为系统需要靠多出来的额外信息管理内存。特别是区块越小,额外负担所占的比例就越大,更加显得浪费。
考虑到小型区块可能造成的内存碎片问题,SGI设计了双层配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128字节时,视之为“足够大”,便调用第一级配置器;当配置区块小于128字节时,视之为“过小",为了降低额外负担,便采用复杂的memory pool整理方式,将会调用二级空间配置器直接去内存池中申请,而不再求助于第一级配置器。整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于__USE_MALLOC是否被定义。
空间配置器文件组成
- <stl_construct.h> 定义了全局函数construct()和destroy(),负责对象的构造和析构。
- <stl_alloc.h> 定义了一二级配置器,配置器统称为alloc而非allocator!
- <stl_uninitialized.h> 定义了一些全局函数,用来填充(fill)或者复制(copy)大块内存数据,也隶属于STL标准规范。
一级空间配置器设计与实现
一级空间配置器只是简单的封装了一下malloc和free实现的。在allocate函数中如果通过malloc申请内存失败(失败返回0)就改用oom_malloc(size_t n)函数尝试分配内存,如果oom发现没有指定new-handler函数的话,那就直接调__THROW_BAD_ALLOC,丢出bad_alloc或是直接通过exit(1)中止程序。
我们来看看allocate函数,这个函数执行的流程就如上面所诉:
static void * allocate(size_t n)
{
void *result = malloc(n);//直接使用第一级分配器,直接使用malloc
if (0 == result) result = oom_malloc(n);//第一级分配器失效了,那就使 用第二级分配器,oom(out of memeory)
return result;//将分配的空间以void*的方式返回,用户可以随意转化为需要的类型
}
再来看看oom_malloc函数是如何实现的,很显然这里的内存不足处理函数是需要客户端来实现的,默认情况下为0,也即默认情况下将会直接执行__THROW_BAD_ALLOC。内存不足处理例程是客端的责任,设定内存不足处理例程也是客端的责任。如果你已经设置了out-of-memory handler,那么系统会调用你设定的处理程序,企图释放内存来给程序用。会一直循环直到成功分配到内存才结束。所以这个函数设计不好的话会出现死循环!这也是为什么STL里面默认没有设置处理机制。
// malloc_alloc out-of-memory handling
// 初值为0.有待客端设定
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
template <int inst>
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);//如果内存已经得到满足的话,那么就可以返回了,如果不满足,那么
//就继续释放,分配....
}
}
过程如下图:
接下来我们看一个简单的vector的实现。
template<typename T>
class Vector
{
public:
// 构造函数
Vector(int size = 0)
:mcur(0), msize(size)
{
mpvec = new T[msize];
}
// 析构函数
~Vector()
{
delete[]mpvec;
mpvec = nullptr;
}
// 拷贝构造函数
Vector(const Vector<T> &src)
:mcur(src.mcur), msize(src.msize)
{
mpvec = new T[msize];
for (int i = 0; i < msize; ++i)
{
mpvec[i] = src.mpvec[i];
}
}
// 赋值重载函数
Vector<T>& operator=(const Vector<T> &src)
{
if (this == &src)
return *this;
delete []mpvec;
mcur = src.mcur;
msize = src.msize;
mpvec = new T[msize];
for (int i = 0; i < msize; ++i)
{
mpvec[i] = src.mpvec[i];
}
return *this;
}
// 尾部插入数据函数
void push_back(const T &val)
{
if (mcur == msize)
resize();
mpvec[mcur++] = val;
}
// 尾部删除数据函数
void pop_back()
{
if (mcur == 0)
return;
--mcur;
}
private:
T *mpvec; // 动态数组,保存容器的元素
int mcur; // 保存当前有效元素的个数
int msize; // 保存容器扩容后的总长度
// 容器2倍扩容函数
void resize()
{
/*默认构造的vector对象,内存扩容是从0-1-2-4-8-16-32-...的2倍方式
进行扩容的,因此vector容器的初始内存使用效率特别低,可以使用reserve
预留空间函数提供容器的使用效率。*/
if (msize == 0)
{
mpvec = new T[1];
mcur = 0;
msize = 1;
}
else
{
T *ptmp = new T[2 * msize];
for (int i = 0; i < msize; ++i)
{
ptmp[i] = mpvec[i];
}
delete[]mpvec;
mpvec = ptmp;
msize *= 2;
}
}
};
// 一个简单的测试类A
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
};
int main()
{
Vector<A> vec(10); // 10表示底层开辟的空间大小,但是却构造了10个A对象
A a1, a2, a3;
cout << "---------------" <<