空间配置器

  • ​​具有次配置力的SGI空间配置器​​
  • ​​SGI标准的空间配置器,std::allocator​​
  • ​​SGI特殊的空间配置器​​
  • ​​构造和析构基本工具:construct()和destroy()​​
  • ​​空间的配置和释放​​
  • ​​第一级配置器__malloc_alloc_template​​
  • ​​第二级配置器__default_alloc_template​​
  • ​​空间配置函数allocate()​​
  • ​​deallocate()同理​​
  • ​​重新填充free lists​​

关于C++new和delete此处不做详解,可见:​​new和delete之美​​ 本篇文章以讨论STL的空间配置器为主

CARE:以STL的运用角度来说,空间配置器是最不需要介绍的东西,他总是隐藏在一切组件的背后,默默工作,默默付出。但若以STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象都存放在容器之中,而容器一定需要配置空间以置放资料。

我们先来设计一个简单的空间配置器

#ifndef __JJALLOC_
#define __JJALLOC_
#include <new.h> // for placement new
#include <stddef.h> // for ptrdiff_t, size_t
#include <stdlib.h> // for exit()
#include <limits.h> // for UINT_MAX
#include <iostream>
namespace JJ {
// 分配内存
template <class T>
// size:表示存储n个T*类型的对象
inline T* _allocate(ptrdiff_t size, T*) {
_set_new_handler(0);
// 分配size*sizeof(T)容量的空间,此时也仅仅是分配内存而已
T* tmp = (T*)(::operator new((size_t)(size*sizeof(T))));
if (tmp == 0) { // 内存分配失败
std::cerr << "out of memory" << std::endl;
exit(1);
}
return tmp;
}
// 释放内存
template <class T>
inline void _deallocate(T* buffer) {
// 把内存归还给系统
::operator delete(buffer);
}
// 初始化对象
template <class T1, class T2>
inline void _construct(T1* p, const T2& value) {
// 此时调用的new就是placement,而不是正常情况下的new operator
new(p) T1(value); // placement new, invoke construct of T1
}
// 调用析构函数
template <class T>
inline void _destroy(T* ptr) {
ptr->~T(); // 调用析构函数释放对象
}

template <class T>
class allocator {
public:
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef size_t size_type;
typedef ptrdiff_t dofference_type;
// 一个嵌套的类模板,拥有唯一成员other
template <class U>
struct rebind {
typedef allocator<U> other;
}
// 配置空间,足以存储n各T对象,第二参数的各提示,实现上可能会用他来增进区域性,或完全忽略他
pointer allocate(size_type n, const void* hint=0) {
return _allocate((difference_type)n, (pointer)0);
}
void deallocate(pointer p, size_type n) { _deallocate(p); }
void construct(pointer p, const T& value) { _construct(p); }
void destroy(pointer p) { _destroy(p); }
// 返回某个对象的地址
pointer address(reference x) { return (pointer)&x; }
// 返回某个const对象的地址
const_pointer const_address(const_reference x) { return (const_pointer)&x; }
// 返回可成功配置的最大量
size_type max_size() const { return size_type(UINT_MAX/sizeof(T)); }
};
}
#endif

将该应用到程序之中,可以发现,他只能有限度的搭配PJ STL和RW STL。
PJ STL未完全遵循STL规格,其所供应的许多容器需要一个非标准的空间配置器接口allocateor::_Charalloc()
RW STL在许多容器身上运用了缓冲区,情况复杂,JJ::allocator无法与之兼容
SGI STL使用了一个专属的,拥有次层配置能力的,效率优越的特殊配置器

#include <vector>
#include <iostream>
#include "jjalloc.h"
int main() {
int ia[5] = { 0, 1, 2, 3, 4 };
unsigned int i;
std::vector<int, JJ::allocator<int>> iv(ia, ia+5);
for (int i = 0; i < iv.size(); i++) {
std::cout << iv[i] << ' ';
}
std::cout << std::endl;
}

具有次配置力的SGI空间配置器

SGI的配置器和标准规范不同,其名称是alloc,而且不接受任何参数
写法

template <class T, class Alloc=alloc>
class vector {...}

SGI标准的空间配置器,std::allocator

虽然SGI也定义一个符合部分标准,名为allocator的配置器,但SGI自己从未用过他,也不建议我们使用,因为其效果不佳,只是把::operator new和::operator delete做一层浅层的包装而已(也可以看作是HP版本的HP default allocator,提供他只是为了回溯兼容)
此处不做讲解

SGI特殊的空间配置器

一般而言,我们所习惯的C++内存配置操作和释放操作是这样的

class Foo {...};
Foo* pf = new Foo; // 配置内存,然后构造对象
delete of; // 将对象析构,然后释放内存

此中new包含两部分操作

  • 调用::operator new配置内存
  • 调用Foo::Foo()构造对象内容

delete也包含两阶段操作

  • 调用Foo::~Foo()将对象析构
  • 调用::operator delete释放内存

为了精密分工,STL allocator决定将这两个阶段操作区分开来,内存配置操作由alloc::allocate()负责,内存释放由alloc::deallocate负责,对象构造操作由::construct()负责,对象析构操作由::destroy()负责

STL标准规格告诉我们,配置器定义于之中,SGI内含以下两个文件

#include <stl_alloc.h>    // 负责内存空间的配置与释放
#include <stl_construct.h> // 负责对象内容的构造和析构

构造和析构基本工具:construct()和destroy()

construct接受一个指针p和一个初值value,将初值设定到指针所指的空间上,C++的placement new可以完成此功能

#include <new.h>
template <class T1, class T2>
inline void construct(T1* p, const T2& value) {
new(p) T1(value);
}

接下来来看看析构工具

  • 第一种接收一个指针,直接析构
  • 第二种接收一个迭代器,如果这个范围很大,而每个对象的析构函数无关痛痒,此时花费效率很高。因此,当__true_type,则什么也不做,若不,就循环寻访整个范围,调用析构函数
// 第一个版本,接受一个指针
template <class T>
inline void destroy(T* pointer) {
pointer->~T(); // 调用析构函数
}
// 第二个版本,接收两个迭代器,此函数设法找出元素的数值型别
// 进而利用__type_traits<>求取最适当措施
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
__destroy(first, last, value_type(first));
}
// 判断元素的型别value_type是否有trivial_destructor
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destroy_aux(first, last, trivial_destructor)
}
// 如果元素的数值型别value_type有non-trivial_destructor
template <class ForwardIterator>
inline __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type) {
for (; first < last; ++first)
destroy(&*first);
}
// 如果元素的数值型别value_type有trivial_destructor
template <class ForwardIterator>
inline __destroy_aux(ForwardIterator first, ForwardIterator last, __true_type) {}

空间的配置和释放

考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层配置器,第一层配置器直接使用malloc和free,第二级配置器视情况采用不同的策略(128bytes),取决于__USE_MALLOC

第一级配置器__malloc_alloc_template

第二级配置器__default_alloc_template

第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的其实不仅是内存碎片,配置时的额外负担也是一个大问题。额外负担永远无法避免,毕竟系统要靠这多出了的空间来管理内存。

当区块内存大于128bytes,就移交第一级配置器处理。当区块小于128bytes,则以内存池(memory pool)管理,此法称为次层配置:每次配置一大块内存,并维护对应之自由链表(free_list),下次若有相同大小的内存需要,就直接从free_lists中拨出,如果客端释还小额区块,就由配置器回收到free_list中。为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需要量上调至8的倍数,并维护16个free_list,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。

union obj {
union obj* free_list_link;
char client_data[1];
}
enum {__ALIGN = 8};   // 小型区块的上调边界
enum {__MAX_BYTES = 128}; // 小型区块的上界
enum {__NFREELISTS = __MAX_BYTES/_ALIGN}; // free_list个数
template <bool threads, int inst>
class __default_alloc_template {
private:
// ROUND_UP将bytes上调到8的倍数
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
private:
union obj {
union obj* free_list_link;
char client_data[1];
}
private:
// 16个free list
static obj* volatile free_list[__NFREELISTS];
// 以下函数根据区块大小,决定使用第n号free-list,n从0起算
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + __ALIGN-1)/(__ALIGN-1));
}
// 返回一个大小为n的对象,并可能加入大小为n的其他区块到free list中
static void* refill(size_t n) {
// 配置一大块空间,可容纳nobjs个大小的“size”的区块
// 如果配置nobjs个区块有所不便,nobjs可能会降低
static char* chunk_alloc(size_t size, int& nobjs;)
}
// 内存池起始位置
static char* start_free;
// 内存池结束位置
static char* end_free;
static size_t heap_size;
public:
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);
}
// 以下是static data member的定义和初值设定
template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = 0;

template <bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = 0;

template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;

template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj* volatile __default_alloc_template<threads, inst>::free_list[__NFREELISTS] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}

空间配置函数allocate()

此函数先判断区块大小,大于128就调用第一级配置器,小于128就检查对应的free list。如果free list之内有可用的区块,就直接拿来用,如果没有,将区块大小上调到8倍数边界,然后调用refill(),准备为free list重新填充空间

static void* allocate(size_t n) {
obj* volatile * my_free_list;
obj* result;
// 大于128就调用第一级配置器
if (n > (size_t) __MAX_BYTES) { return (malloc_alloc::allocate(n)); }
// 寻找16个free list中适当的一个
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if (result == 0) {
// 没找到可用的free list,准备填充free list
void* r = refill(ROUND_UP);
return r;
}
// 调整free list
*my_free_list = result->free_list_link;
return result;
}

deallocate()同理

static void* deallocate(void* p, size_t n) {
obj* volatile * my_free_list;
obj* q = (obj*)p;
// 大于128就调用第一级配置器
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
my_free_list = free_list + FREELIST_INDEX(n);
// 调整free list
q->free_list_link = *my_free_list;
*my_free_list = q;;
}

重新填充free lists

当free list没有可用区块的时,就调用refill(), 准备为free list重新填充空间,新的空间将取自内存池(经由chunk_alloc完成)。缺省取得20个新结点(新区块),但玩意内存池空间不足,获得的结点数(区块数)可能小于20

// 返回一个大小为n的对象,并且有时候会为适应的free list增加节点
// 假设n已经适当上调至8的倍数
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n) {
int nobjs = 20;
// 调用chunk_alloc,尝试取得nobjs个区块作为free_list的新结点
char* chunk = chunk_alloc(n, nobjs);
obj* volatile* my_free_list;
obj* result;
obj* current_obj, *next_obj;
int i;
// 如果只获得一个区块,那么这个区块就分配给调用者,free list无新结点
if (1 == nobjs) return chunk;
// 否则准备调整free list,纳入新结点
my_free_list = free_list + FREELIST_INDEX(n);
// 以下在chunk空间建立free list
result = (obj*)chunk; // 这一块内存返回给客端
// 以下导引free list指向新配置的空间(取自内存池)
*my_free_list = next_obj = (obj*)(chunk _ n);
// 以下将free list的各节点串接起来
for (i = 1; ; i++) // 从1开始,因为第0个给了客端
current_obj = next_obj;
next_obj = (obj*)*((char*)next_obj + n);
if (nobjs - 1 == i) {
current_obj_obj->free_list_link = 0;
break;
}
else {
current_obj->free_list_link = next_obj;
}
return result;
}