一、背景
STL的 操作对象都存储在容器(vector,deque,list)内,而容器对象是一定要配置内存空间的,每一种容器的内存都是通过空间分配器(allocator)实现的。
空间分配器allocator
,这是一种用来分配和管理内存的模板类。allocator
是STL的一部分,用于管理动态分配的内存,它为对象提供内存,而不像常规 new 和 delete 运算符那样分别为每个对象调用构造函数和析构函数。allocator
的使用可以提高内存分配和管理的效率,减少内存碎片的产生,提高程序的性能。
注:为什么不说 allocator 是内存分配器而说它是空间分配器呢?因为空间不一定是内存,空间也可以是磁盘或者其它辅助存储介质。
举个例子,vector 的默认空间分配器allocator,它的调用过程如下:
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int, allocator<int>> v = {1, 2, 3, 4, 5}; // 默认空间分配器allocator<int>
for (auto i : v) {
cout << i << " ";
}
cout << endl;
return 0;
}
其中,vector
的第二个参数是allocator<int>
,表示要使用的空间分配器类型是allocator<int>
。
可以看到,vecor等容器的空间配置包括了内存分配、对象构造、析构对象、内存回收/释放。那么, 空间分配器的原理到底是什么样的呢?
二、allocator
的常用方法
allocator
的常用方法包括allocate
和deallocate
,用于分配和释放内存。用法如下:
#include <iostream>
#include <memory>
using namespace std;
int main() {
allocator<int> alloc;
int* ptr = alloc.allocate(5);
for (int i = 0; i < 5; i++) {
ptr[i] = i;
}
for (int i = 0; i < 5; i++) {
cout << ptr[i] << " ";
}
cout << endl;
alloc.deallocate(ptr, 5);
return 0;
}
其中,allocate
方法分配5个整型空间,并返回指向第一个整型的指针。deallocate
方法释放内存,参数是指向首元素的指针和要释放的元素个数。
三、空间分配器的两级实现
很容易想象,为了实现空间配置器,完全可以利用new和delete函数并对其进行封装实现STL的空间配置器。但是,为了最大化提升效率,SGI STL版本并没有简单的这样做,而是采取了一定的措施,实现了更加高效复杂的空间分配策略。
在SGI STL中,将对象的构造分成内存空间配置和对象构造两部分:
内存配置操作: 通过alloc::allocate()实现
内存释放操作: 通过alloc::deallocate()实现
对象构造操作: 通过::construct()实现
对象释放操作: 通过::destroy()实现
考虑到小型内存块所可能造成的内存碎片的问题,SGI STL采用了两级配置器:
第一级:大型内存分配直接使用malloc()和free()函数进行处理。大型内存的分配不仅仅是内存大小的问题,更重要的是内存的使用情况。在程序运行时,很难预先预测到程序需要的内存大小,因此当内存需求较大、难以确定时,就需要使用malloc()和free()进行内存分配和释放。
第二级:小型内存分配采用固定大小的内存池进行分配。一般使用链表来保存已经分配过的、但是现在处于空闲状态的内存块。每次请求内存时,遍历这个链表,找到第一个合适的内存块返回。如果这个链表里没有可用的内存块,则向操作系统请求更多的内存,并继续维护链表。
为了最大化解决内存碎片问题,提升效率,当配置的区块超过 128 bytes 时,视之为 ”足够大“,便调用第一级配置器;当配置区块小于 128 bytes 时,则视之为 ”过小“,便采用内存池方式,而不再求助于第一级配置器。默认使用第二级配置器。
一级空间配置器 allocate
template <class T>
inline T* allocate(ptrdiff_t size, T*) {
set_new_handler(0); // 设置内存申请失败的回调函数
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) { // 申请内存失败
cerr < "out of memory" << endl;
exit(1):
}
return tmp;
}
template <class T>
inline void deallocate(T*buffer) {
::operator delete(buffer);
}
二级空间配置器 allocate
public:
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
else {
_Obj* __STL_VOLATILE* __my_free_list // —— _Obj** 二级指针 + volatile
= _S_free_list + _S_freelist_index(__n);
// Acquire the lock here with a constructor call.
// This ensures that it is released in exit or during stack
// unwinding.
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance; // 临界区代码
# endif
_Obj* __RESTRICT __result = *__my_free_list; // 编号链的对应编号位置下指向其free list的空表第一个空位置的指针
if (__result == 0)
__ret = _S_refill(_S_round_up(__n)); // 分配内存池
else {
*__my_free_list = __result -> _M_free_list_link; // 指向下一个节点,嵌入式指针
__ret = __result;
}
}
return __ret; // 返回当前free list的第一个空闲位置
};
四、空间分配器 allocators的对外接口
通过实现重载这些成员函数可以实现内存池。SGI STL 中考虑到了内存分配失败的异常处理,内置轻量级内存池(主要用于处理小块内存的分配,应对内存碎片问题)实现, 多线程中的内存分配处理(主要是针对内存池的互斥访问)等。
typedef unsigned int size_t;
allocator::value_type // 数值类型 typedef T
allocator::pointer // 指针 typedef T*
allocator::const_pointer // 常指针 typedef const T*
allocator::reference // 引用 typedef T&
allocator::const_reference // 常引用 typedef const T&
allocator::size_type // 长度类型 typedef size_t
allocator::difference_type // typedef ptrdiff_t
/*ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。*/
allocator::rebind // 重新绑定
/*
rebind
一个嵌套的 nested(嵌套) class template(类模板) 。class rebind<U> 拥有唯一成员 other,那么是一个typedef(定义类型),代表allocator<U> 配置U类型的空间
给定了类型T的分配器Allocator=allocator<T>,现在想根据相同的策略得到另外一个类型U的分配器allocator<U>,那么allocator<U>=allocator<T>::Rebind<U>::other.
*/
allocator::allocator() // default constructor(默认构造函数)
allocator::allocator(const allocator&) // copy constructor(拷贝构造函数)
template<class U> allocator::allocator(const allocator<U>&) // 泛化的拷贝构造函数
allocator::~allocator() // destructor 析构函数
pointer allocator::address(reference x) const//返回对象的地址
const_pointer allocator::address(const_reference x) const // 返回const 对象的地址
pointer allocator::allocate(size_type n,const void * = 0)
/*
配置空间,足以存储n个T对象
参数提示,可以用他增进区域性
*/
void allocator::deallocator(pointer p,size_type n)// 归还先前配置的空间
size_type allocator::max_size() const //返回可成功配置的最大空间
void allocator::construct(ponter p,const T& x) // 等价于new((void *)p) T(x)
void allocator::destroy(pointer p)// 等同于p->~T()
五、自定义空间分配器
以下是一个简单的空间配置器的实现:
#include <cstdio>
#include <cstdlib>
#define __THROW_BAD_ALLOC printf("out of memory"); exit(1)
template<class T>
inline T* _allocate(ptrdiff_t size, T*) {
set_new_handler(0);
T* tmp = (T*)(::operator new((size_t)(size * sizeof(T))));
if (tmp == 0) {
__THROW_BAD_ALLOC;
}
return tmp;
}
template<class T>
inline void _deallocate(T* buffer) {
::operator delete(buffer);
}
// 配置一个元素空间,space为元素的大小
template<class T>
class alloc {
public:
static T* allocate() {
return _allocate(1, (T*)0);
}
static T* allocate(size_t n) {
return n == 0 ? 0 : _allocate(n, (T*)0);
}
static void deallocate(T* buffer) {
_deallocate(buffer);
}
static void construct(T* ptr) {
new (ptr) T(); // 在已分配空间的地址上构造一个 T 类型的对象
}
static void destroy(T* ptr) {
ptr->~T(); // 手动调用 T 类型对象的析构函数
}
};
其中,_allocate
是分配函数,_deallocate
是释放函数。alloc
是模板类,它的静态方法allocate
用于分配内存,deallocate
用于释放内存,construct
用于在已分配的内存中构造一个对象,destroy
用于销毁一个对象。
以下是一个简单的两级空间分配器的实现,代码如下:
#include <cstdlib>
#include <cstddef>
#include <new>
class Allocator {
public:
void *allocate(size_t size) {
// 对于小型内存的分配,以8个字节的字节为块大小进行管理
if (size <= kMaxBytes) {
int index = freeListIndex(size);
obj *list = freeList[index];
if (list) {
freeList[index] = list->next;
return list;
}
// 如果没有空闲的内存块,则重新申请以kAlign字节对齐的内存,并把超过需要的部分添加到内存池之中
else {
return refill(roundUp(size));
}
}
// 对于大型内存的分配,直接使用malloc进行分配
else {
return malloc(size);
}
}
void deallocate(void *p, size_t size) {
// 对于小型内存的释放,回收到内存池之中
if (size <= kMaxBytes) {
int index = freeListIndex(size);
obj *list = (obj *) p;
list->next = freeList[index];
freeList[index] = list;
}
// 对于大型内存的释放,直接使用free()函数释放内存
else {
free(p);
}
}
private:
static const int kMaxBytes = 128;
static const int kAlign = 8;
static const int kNumFreeLists = kMaxBytes / kAlign;
union obj {
union obj *next;
char clientData[1];
};
obj *volatile freeList[kNumFreeLists] = {nullptr};
// 返回大小为nBytes的内存块应该在连接的链表里的位置
int freeListIndex(size_t nBytes) const {
return (nBytes + kAlign - 1) / kAlign - 1;
}
// 将内存块添加到连接的链表里
void *refill(size_t nBytes) {
int nObjs = 20;
char *chunks = chunkAlloc(nBytes, nObjs);
if (nObjs == 1) {
return chunks;
}
obj *list = freeList[freeListIndex(nBytes)];
obj *curObj = (obj *) chunks;
obj *nextObj = (obj *) (chunks + nBytes);
for (int i = 0;;) {
curObj[i].next = &curObj[i+1];
if (++i == nObjs - 1) {
curObj[i].next = list;
break;
}
}
freeList[freeListIndex(nBytes)] = &curObj[0];
return chunks;
}
// 从系统中申请内存,并且将超出部分添加到内存池之中
char *chunkAlloc(size_t size, int &nObjs) {
size_t totalBytes = nObjs * size;
size_t bytesLeft = endFree - startFree;
char *result;
if (bytesLeft >= totalBytes) {
result = startFree;
startFree += totalBytes;
return result;
}
else if (bytesLeft >= size) {
nObjs = bytesLeft / size;
totalBytes = size * nObjs;
result = startFree;
startFree += totalBytes;
return result;
}
else {
size_t bytesToGet = 2 * totalBytes + roundUp(heapSize >> 4);
if (bytesLeft > 0) {
obj *list = freeList[freeListIndex(bytesLeft)];
((obj *) startFree)->next = list;
freeList[freeListIndex(bytesLeft)] = (obj *) startFree;
}
startFree = (char *) malloc(bytesToGet);
if (!startFree) {
obj *list;
for (int i = size; i <= kMaxBytes; i += kAlign) {
list = freeList[freeListIndex(i)];
if (list) {
startFree = (char *) list;
endFree = startFree + i;
freeList[freeListIndex(i)] = list->next;
return chunkAlloc(size, nObjs);
}
}
endFree = nullptr;
startFree = (char *) allocate(bytesToGet);
}
heapSize += bytesToGet;
endFree = startFree + bytesToGet;
return chunkAlloc(size, nObjs);
}
}
// 对齐,即将n按8对齐
size_t roundUp(size_t n) const {
return (n + kAlign - 1) & ~(kAlign - 1);
}
// 内存池的起始地址
static char *startFree;
// 内存池的结束地址
static char *endFree;
// 内存池总共的大小
static size_t heapSize;
};
char *Allocator::startFree = nullptr;
char *Allocator::endFree = nullptr;
size_t Allocator::heapSize = 0;
两级空间分配器可以根据不同大小的内存要求采用不同的原理,从而更加灵活地分配和管理内存。这种实现方式在许多高效的C++库中使用,例如STL和Boost等。
参考:
c++实现一个简单的空间配置器allocator_swffsdgasdg的博客-CSDN博客
《STL源码剖析》提炼总结:空间配置器(allocator) - 知乎