文章目录
- 1、allocator 接口层
- 1.1、接口层源码
- 1.2、定位 new 表达式
- 1.3、实例
- 2、allocator 实现层
- 2.1、一级空间配置器
- 2.2、二级空间配置器
- allocate
- deallocate
- 源码流程分析
- 3、allocator 问题
背景:频繁使用 malloc 分配内存的造成的问题:
- 系统调用,系统开销较大
- 产生大量的内存碎片(外部碎片)。
注:内存碎片
- 内部碎片:页式管理、段式管理、段页式管理,无法避免,可以通过算法优化。
- 外部碎片:申请堆空间之间的片段空隙,空间配置器优化的是外部碎片。
因此,引入空间配置器 allocator。可以感知类型的空间分配器,用于分配和释放内存,将内存的分配释放与对象的创建销毁分开。
1、allocator 接口层
1.1、接口层源码
针对容器来说,空间的分配 construct
与对象的创建 allocate
是分开的,并不是绑定在一起。提供的接口如下:
// 1、空间分配
// 1.1、申请未初始化的空间
pointer allocate( size_type n, const void * hint = 0 );
T* allocate( std::size_t n, const void * hint);
T* allocate( std::size_t n );
// 1.2、释放空间
void deallocate( T* p, std::size_t n );
// 2、对象创建
// 2.1、对象的构建。
void construct( pointer p, const_reference val );
// 2.2、对象的销毁
void destroy( pointer p );
源码位于stl_alloc.h
,接口层的源码如下:
// 空间配置器的接口层
template <class _Tp>
class allocator {
typedef alloc _Alloc;
_Tp* allocate(size_type __n, const void* = 0) {
return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n * sizeof(_Tp))) : 0;
}
void deallocate(pointer __p, size_type __n)
{ _Alloc::deallocate(__p, __n * sizeof(_Tp)); }
void construct(pointer __p, const _Tp& __val) { new(__p) _Tp(__val); }
void destroy(pointer __p) { __p->~_Tp(); }
};
1.2、定位 new 表达式
定位 new 表达式:在 p 指向的未初始化的内存中构造 T
类型对象。
void construct(pointer __p, const _Tp& __val) { new(__p) _Tp(__val); }
1.3、实例
例:使用std::allocator
实现自定义的Vector
类
vector 模型
Vector模型
______________________________
|_|_|_|_|_|____________________|
↑ ↑ ↑
_start _finish _end_of_storage
接口形式:
template<typename T>
class Vector
{
public:
Vector();
~Vector();
void push_back(const T &);
void pop_back();
int size();
int capacity();
private:
void reallocate();//重新分配内存,动态扩容要用的
private:
static std::allocator<T> _alloc;
T *_start; // 指向数组中的第一个元素
T *_finish; // 指向最后一个实际元素之后的那个元素
T *_end_of_storage; // 指向数组本身之后的位置
};
实现代码:
#include <algorithm>
#include <iostream>
#include <memory>
using std::cout;
using std::endl;
template<typename T>
class Vector {
public:
Vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
~Vector();
void push_back(const T &);
void pop_back();
int size() const { return _finish - _start; }
int capacity() const { return _end_of_storage - _start; }
// 定义迭代器
typedef T * iterator;
typedef const T * const_iterator;
const_iterator begin() const { return _start; }
const_iterator end() const { return _finish; }
private:
void reallocate(); // 重新分配内存,动态扩容
private:
// 定义空间配置器,静态共享
static std::allocator<T> _alloc;
T *_start; //指向数组中的第一个元素
T *_finish; //指向最后一个实际元素之后的那个元素
T *_end_of_storage; //指向数组本身之后的位置
};
// 初始化 allocator
template <class T>
std::allocator<T> Vector<T>::_alloc;
template <typename T>
void Vector<T>::push_back(const T & t)
{
// 数组扩容
if(size() == capacity()) {
reallocate();
}
// 在 finish 所指位置构造对象
if(size() < capacity()) {
_alloc.construct(_finish++, t);
}
}
template <typename T>
void Vector<T>::pop_back()
{
if(size() > 0) {
_alloc.destroy(--_finish);
}
}
template <typename T>
Vector<T>::~Vector()
{
// 1. 先析构对象
while(_finish != _start) {
_alloc.destroy(--_finish);
}
// 2. 回收空间
if(_start) {
_alloc.deallocate(_start, capacity());
}
}
template <typename T>
void Vector<T>::reallocate() // 重新分配内存,动态扩容
{
// 当 size == capacity,需要动态扩容
int oldCapacity = capacity();
int newCapacity = (oldCapacity == 0 ? 1 : 2 * oldCapacity); // 2 倍扩容
// 1、开辟新的空间
T * ptmp = _alloc.allocate(newCapacity);
if(_start) {
// 2、将原来空间上的对象复制到新空间
// 新开辟的空间上没有对象,无法使用 copy, memcpy 等算法
std::uninitialized_copy(_start, _finish, ptmp);
// 3、释放原来的空间
// 3.1、销毁原来空间上的对象
while(_finish != _start) {
_alloc.destroy(--_finish);
}
// 3.2、释放原来的空间
_alloc.deallocate(_start, oldCapacity);
}
// 5. 重置3个指针, 指向新的空间
_start = ptmp;
_finish = _start + oldCapacity;
_end_of_storage = _start + newCapacity;
}
template <class Container>
void print(const Container & c)
{
// for 循环需要迭代器,自定义 vector 需要提供迭代器
for( auto & elem : c) {
cout << elem << " ";
}
cout << endl;
}
void test0 () {
Vector<int> vec;
vec.push_back(1);
cout << "vec's size:" << vec.size() << endl;
cout << "vec's capa:" << vec.capacity() << endl;
vec.push_back(2);
cout << "vec's size:" << vec.size() << endl;
cout << "vec's capa:" << vec.capacity() << endl;
vec.push_back(3);
cout << "vec's size:" << vec.size() << endl;
cout << "vec's capa:" << vec.capacity() << endl;
vec.push_back(4);
cout << "vec's size:" << vec.size() << endl;
cout << "vec's capa:" << vec.capacity() << endl;
vec.push_back(5);
cout << "vec's size:" << vec.size() << endl;
cout << "vec's capa:" << vec.capacity() << endl;
print(vec);
}
int main(void) {
test0();
return 0;
}
2、allocator 实现层
真正进行分配空间的是std::alloc
,见源码:
template <class _Tp>
class allocator {
typedef alloc _Alloc;
_Tp* allocate(size_type __n, const void* = 0) {
return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n * sizeof(_Tp))) : 0;
}
};
使用两级空间配置器。在分配空间的时候,若申请的空间大小:
- 大于 128 字节,调用一级配置器,使用 malloc / free
- 小于等于 128 字节,调用二级配置器(默认空间配置器),使用内存池 + 16个自由链表。
// 空间配置器的实现层
// 1、一级配置器,使用 malloc 进行空间分配
# ifdef __USE_MALLOC
typedef malloc_alloc alloc;
// 2、二级配置器,默认的空间分配器,使用内存 + 16个自由链表
# else
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
# endif
2.1、一级空间配置器
对于大块内存(大于 128 字节),第一级空间配置器使用 malloc / free 分配和释放内存,对应的源码:
// 一级空间配置器(对 malloc/free 的封装)
template <int __inst>
class __malloc_alloc_template {
public:
// allocate 接口:封装 malloc
static void* allocate(size_t __n)
{
void* __result = malloc(__n);
if (0 == __result) __result = _S_oom_malloc(__n);
return __result;
}
// deallocate 接口:封装 free
static void deallocate(void* __p, size_t /* __n */)
{
free(__p);
}
...
};
// 定义 alloc 类
typedef __malloc_alloc_template<0> malloc_alloc
typedef malloc_alloc alloc
2.2、二级空间配置器
默认空间配置器,所有的容器最终调用它完成空间的分配和释放。由内存池 + 16 个自由空闲链表组成。
简单来说,allocator
根据用户申请的空间大小(8 的整数倍,向上取整),先向堆申请大块内存,然后拿出其中一部分将其分割成固定大小的内存块,并组织成空闲链表;另一部分作为内存池备用。此后,每次申请内存先向空闲链表申请空间,对应的空闲链表不存在则向内存池申请空间,否则向堆申请大块内存。
二级空间配置器源码部分如下:
typedef __default_alloc_template alloc
template <bool threads, int inst>
class __default_alloc_template {
public:
static void* allocate(size_t __n);
static void deallocate(void* __p, size_t __n)
private:
// 内存分配
// 1、填充自由空闲链表
static void* _S_refill(size_t __n);
// 2、申请内存
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// 通过申请的字节数,得到自由空闲链表的下标
// 例如申请 25~32 个字节,对应的链表下标就是 3
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}
// 将申请的字节数,向上取整,得到 8 的整数倍,申请连续的空间
static size_t _S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }
private:
enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16};
// 链表结点(指针)
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1];
};
// 16 个自由空闲链表
static _Obj* _S_free_list[_NFREELISTS];
// 内存池,一次申请大空间,之后只需要在内存池上进行切割
static char* _S_start_free; // 内存池的起始位置
static char* _S_end_free; // 内存池的结束位置
static size_t _S_heap_size; // 16个自由空闲链表 + 内存池,即堆上申请的总空间
};
allocate
static void* allocate(size_t __n) // __n 要申请的字节数
{
void* __ret = 0;
// 1、当__n > 128 字节时,使用一级空间配置器完成空间的申请
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n);
}
// 2、当 __n <= 128 字节时,使用二级空间配置器完成空间的申请
else {
// 通过 __n 的值获取对应的到自由的空闲链表。
// _S_freelist_index 函数根据 __n 的值获得链表下标
_Obj** __my_free_list = _S_free_list + _S_freelist_index(__n);
_Obj* __result = *__my_free_list;
// 第一次申请 __n 空间,链表结点地址是 0,堆上还未申请这条自由链表
if (__result == 0)
// 从堆空间中去找内存块,填充该自由空闲链表。
// _S_round_up 函数申请的空间大小向上取整,是 8 的倍数
__ret = _S_refill(_S_round_up(__n));
else {
// 当再次申请 __n 空间,直接从已经申请的链表头部取下链表结点,时间复杂度 O(1)
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
}
空间分配的核心函数在于:_S_refill
和 _S_chunk_alloc
函数
_S_refill
函数:填充__nobjs
个结点大小为 __n
的自由空闲链表。其中__nobjs
默认是 20,若内存池不够分割,则重新计算。
// _S_refill 函数:填充自由空闲链表
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
// 申请 20 个 __n 字节大小的大块内存
char* __chunk = _S_chunk_alloc(__n, __nobjs);
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
if (1 == __nobjs) return(__chunk);
// 根据 __n 获取对应的空闲链表下标
__my_free_list = _S_free_list + _S_freelist_index(__n);
/* Build free list in chunk */
// 将申请的大块内存 chunk 切分 __nobjs 个空间大小为 __n 的链表结点,并组织成一条自由空闲链表
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
}
else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
}
_S_chunk_alloc
函数:申请空间。比较内存池剩余空间与本次申请的内存空间大小:
- 内存池剩余空间足够,则内存池一次分配相应大小的内存
- 内存池剩余空间不足,则判断剩余空间能否分割,若不能分割,则向堆上申请大块内存
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
int& __nobjs)
{
char* __result;
// 本次申请的内存块大小
size_t __total_bytes = __size * __nobjs;
// 内存池剩余空间大小
size_t __bytes_left = _S_end_free - _S_start_free;
// 1、内存池剩余空间大小 > 本次申请的内存块大小,
// 给它一次分配本次要申请的内存块大小
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
}
// 2、内存池剩余空间大小 < 本次申请的内存块大小,则判断内存池剩余空间大小是否足够分割
// 2.1、内存池剩余空间够分割,将内存池剩余空间分割成固定大小的内存块(用户指定大小__size )
else if (__bytes_left >= __size) {
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
}
// 3、剩余的堆空间不够切分:__bytes_left = 0 < __size
// 重新向堆上申请空间
else {
// 计算需要申请的空间大小
size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 内存池开始申请空间
_S_start_free = (char*)malloc(__bytes_to_get);
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
// 递归调用
return(_S_chunk_alloc(__size, __nobjs));
}
}
deallocate
- 回收的空间大于 128 字节,直接使用 free 回收空间
- 回收的空间小于等于 128 字节,直接链回自由空闲链表
static void deallocate(void* __p, size_t __n)
{
// 回收的空间大于 128 字节,直接使用 free 回收空间
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
// 回收的空间小于等于 128 字节,采用链表头插法,将空闲链表结点重新挂回到空闲链表
else {
// 获取空闲链表的首地址
_Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n);
// 链表的头插法
_Obj* __q = (_Obj*)__p;
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
}
}
源码流程分析
第一次申请 32 字节的空间
// 1、调用 _S_refill 函数
int __nobjs = 20;
// 向 _S_chunk_alloc 申请 20 * 32 = 640 空间
char* __chunk = _S_chunk_alloc(__n, __nobjs);
// 2、调用 _S_chunk_alloc 函数
size_t __total_bytes = __size * __nobjs; // __total_bytes = 32 * 20 = 640
size_t __bytes_left = _S_end_free - _S_start_free; // __bytes_left = 0
// 第一次申请空间 __bytes_left = 0 < __size
else {
// __bytes_to_get = 2 * 640 + 0 = 1280
size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 向内存池申请 1280 个字节的空间
_S_start_free = (char*)malloc(__bytes_to_get);
// _S_heap_size = 1280
_S_heap_size += __bytes_to_get;
// _S_end_free = 1280
_S_end_free = _S_start_free + __bytes_to_get;
// 递归调用自身
return(_S_chunk_alloc(__size, __nobjs));
}
// 3、递归调用 _S_chunk_alloc 函数
char* __result;
size_t __total_bytes = __size * __nobjs; // 32 * 20 = 640;
size_t __bytes_left = _S_end_free - _S_start_free; // 1280 - 0 = 1280;
// 1280 > 640
if (__bytes_left >= __total_bytes) {
__result = _S_start_free; // __result = 640
_S_start_free += __total_bytes; // _S_start_free = 640
return(__result);
}
// 5、返回 _S_refill 函数
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 将 640 空间大小切分成 20 个空间大小为 32 字节的链表结点,并组织成一条链表
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
}
else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
如图所示,可以看到 stl::allocator 内存空间消耗较大,只需要一个 32 字节的空间,就一次性申请了 1280 个字节,此时 640 字节的空间作为空闲链表,640 空间作为内存池备用。
第二次申请 64 字节的空间
// 1、调用 _S_refill 函数
int __nobjs = 20;
// 向 _S_chunk_alloc 申请 20 * 64 = 1280 空间
char* __chunk = _S_chunk_alloc(__n, __nobjs);
// 2、调用 _S_chunk_alloc 函数
size_t __total_bytes = __size * __nobjs; // __total_bytes = 64 * 20 = 1280
size_t __bytes_left = _S_end_free - _S_start_free; // __bytes_left = 1280 - 640 = 640
// __bytes_left: 640 > __size: 64
else if (__bytes_left >= __size) {
// 剩余空间可以被分割为 640 / 64 = 10 个节点
__nobjs = (int)(__bytes_left/__size);
__total_bytes = __size * __nobjs;
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
}
// 3、返回 _S_refill 函数
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 将 640 空间大小切分成 10 个空间大小为 64 字节的链表结点,并组织成一条链表
// 此时申请的堆空间全部分配完毕
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
}
else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
如图所示,内存池全部用于分配内存,申请的堆空间耗尽。生成的空闲链表,分配一个 64 字节的结点,剩余 9 个结点。
第三次申请 96 字节的空间
// 1、调用 _S_refill 函数
int __nobjs = 20;
// 向 _S_chunk_alloc 申请 96 * 20 = 1920 空间
char* __chunk = _S_chunk_alloc(__n, __nobjs);
// 2、调用 _S_chunk_alloc 函数
size_t __total_bytes = __size * __nobjs; // __total_bytes = 96 * 20 = 1920
size_t __bytes_left = _S_end_free - _S_start_free; // __bytes_left = 0
// 第一次申请空间 __bytes_left = 0 < __size
else {
// __bytes_to_get = 2 * 1920 + 80 = 3920
size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// 向内存池申请 3920 个字节的空间
_S_start_free = (char*)malloc(__bytes_to_get);
// _S_heap_size = 1280 + 3920 = 5200
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
// 递归调用自身
return(_S_chunk_alloc(__size, __nobjs));
}
// 3、递归调用 _S_chunk_alloc 函数
char* __result;
size_t __total_bytes = __size * __nobjs; // 96 * 20 = 1920;
size_t __bytes_left = _S_end_free - _S_start_free; // 3920;
// 3920 > 1920
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
}
// 5、返回 _S_refill 函数
__result = (_Obj*)__chunk;
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);
// 将 1920 空间大小切分成 20 个空间大小为 96 字节的链表结点,并组织成一条链表
for (__i = 1; ; __i++) {
__current_obj = __next_obj;
__next_obj = (_Obj*)((char*)__next_obj + __n);
if (__nobjs - 1 == __i) {
__current_obj -> _M_free_list_link = 0;
break;
}
else {
__current_obj -> _M_free_list_link = __next_obj;
}
}
return(__result);
第四次申请 72 字节的空间,堆空间耗尽,内存池耗尽
向后面大于 72 字节的链表借用结点,例如 96,将其分割为 24 + 72。
if (0 == _S_start_free) {
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
// if (i = 72; i <= 128; i += 8)
for (__i = __size;
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
// 找到了链表结点(96 > 72)
// 将该 96 字节的结点分配给它
if (0 != __p) {
*__my_free_list = __p -> _M_free_list_link;
_S_start_free = (char*)__p;
_S_end_free = _S_start_free + __i;
return(_S_chunk_alloc(__size, __nobjs));
}
}
3、allocator 问题
内存空间消耗比较大,内存不可控。如果硬件的性能不支持,例如嵌入式系统,则不能使用 std::allocator
做内存的分配。