目录
SGI STL空间配置器
谈一谈为什么需要空间配置器
源于C++new和delete在容器中直接使用的弊端
new=get memory + _construct(T())
delete=free memory + _destroy(~T())
上述这种形式, 单独创建一个对象的时候使用, 完全没有问题. 但是如果在容器书写的时候, 我们直接的使用new + delete去申请容器的底层内存 + 释放容器的底层内存. 是否合理???
内存申请和对象构造的强耦合, 导致仅需要内存申请的时候 存在大量没必要的构造T()
/*
eg : 我自己写了一个vector<int>.
现在我需要先申请一个n = 10的容器.
我本意只是需要这块空间, 但是你却直接给我构造了对象
咋地, 调构造不要钱(调用函数的消耗)是吧
同样, 我析构的时候.它是整个容器都调用了~T(),
但是可能我的有效对象根本没有装满容器
*/
namespace myvector
{
template<class InputIteractor>
void for_each(InputIteractor first, InputIteractor last) {
while (first != last) {
cout << *first++ << " ";
}
cout << endl;
}
template<class T>
class vector {
typedef T* iterator;
typedef const T* const_iterator;
public:
//null obj constructor
vector() : _first(0), _last(0), _end_of_storage(0) {}
vector(int n)
: _first(new T[n]), _last(_first + n), _end_of_storage(_last) {
}
template<class InputIteractor>
vector(InputIteractor first, InputIteractor last)
:_first(0), _last(0), _end_of_storage(0) {
size_t _cap = size_t(last - first);
_first = new T[_cap];
_last = _first + _cap;
_end_of_storage = _last;
iterator iter = _first;
while (first != last) {
(*iter) = *first++;
++iter;
}
}
iterator begin() {
return _first;
}
const_iterator begin() const {
return _first;
}
iterator end() {
return _last;
}
const_iterator end() const {
return _last;
}
size_t size() {
return size_t(_last - _first);
}
size_t capacity() {
return size_t(_end_of_storage - _first);
}
T& operator[](size_t ind) {
return *(_first + ind);
}
const T& operator[](size_t ind) const {
return *(_first + ind);
}
~vector() {
if (_first) {
delete[] _first;
}
_first = nullptr;
_last = nullptr;
_end_of_storage = nullptr;
}
private:
/*
数据结构写法
size_t _size;
size_t _cap;
T* _data;
*/
//stl 迭代器设计
iterator _first;
iterator _last;
iterator _end_of_storage;
//Alloc _allocator;
};
}
int main() {
myvector::vector<Test> ve(5);
return 0;
}
ans
上述想表达啥意思??? 我后序如果需要插入元素, 我自己会创建元素插入, 但是我此时仅仅只是需要这样一个空间, 但是直接使用new之后的结果则是给每个对象都调用了一次T()构造了一次
浪费. 几个对象, 或者默认构造不复杂还OK, 如果对象数目大,或者构造耗时长. 这个也许是一个性能的退化.
所以分离内存管理 + 对象管理是一个可以优化的点. Allocator空间配置器作为第二个参数, 独立容器内存的分配管理很有必要
看如下版本
//其他几乎是一摸一样的和数据结构的正常写法, 核心所在就是那点iterator的设计.
namespace myvector
{
template<class T>
struct Allocator {
T* allocate(size_t size) {//仅malloc
cout << "allocate: " << size << endl;
return (T*)malloc(sizeof(T) * size);
}
void deallocate(void* p) {//仅free
cout << "deallocate" << endl;
free(p);
}
void constructor(T* p, const T& val) {//定位new
cout << "place new" << endl;
new (p) T(val); //定位 new
}
void destroy(T* p) {//仅销毁对象资源
cout << "destroy" << endl;
p->~T();//调用析构
}
};
template<class InputIteractor>
void for_each(InputIteractor first, InputIteractor last) {
while (first != last) {
cout << *first++ << " ";
}
cout << endl;
}
template<class T, class Alloc = Allocator<T>>
class vector {
typedef T* iterator;
typedef const T* const_iterator;
public:
//null obj constructor
vector() : _first(0), _last(0), _end_of_storage(0) {}
vector(int n) {
_first = _allocator.allocate(n);
_last = _first;//起始没有元素
_end_of_storage = _first + n;
}
template<class InputIteractor>
vector(InputIteractor first, InputIteractor last)
:_first(0), _last(0), _end_of_storage(0) {
size_t _cap = size_t(last - first);
_first = new T[_cap];
_last = _first + _cap;
_end_of_storage = _last;
iterator iter = _first;
while (first != last) {
(*iter) = *first++;
++iter;
}
}
iterator begin() {
return _first;
}
const_iterator begin() const {
return _first;
}
iterator end() {
return _last;
}
const_iterator end() const {
return _last;
}
size_t size() {
return size_t(_last - _first);
}
size_t capacity() {
return size_t(_end_of_storage - _first);
}
T& operator[](size_t ind) {
return *(_first + ind);
}
const T& operator[](size_t ind) const {
return *(_first + ind);
}
~vector() {//消除内存 + 析构有效空间对象
//先析构有效对象
for (int i = 0; i < size(); ++i) {
_allocator.destroy(_first + i);
}
_allocator.deallocate(_first);//free内存
_first = nullptr;
_last = nullptr;
_end_of_storage = nullptr;
}
private:
/*
数据结构写法
size_t _size;
size_t _cap;
T* _data;
*/
//stl 迭代器设计
iterator _first;
iterator _last;
iterator _end_of_storage;
Alloc _allocator;
};
}
ans
完全给避免了没有必要的T()和~T()调用
Alloc是可以进行选择的. 说明存在多种空间配置器. 甚至我们可以自己实现. 同样, STL中也是提供了两种不同的Alloc空间配置器. 底层采取宏定义的形式限制默认空间配置器是alloc ( 二级空间配置器SGI STL内存池 )
一级空间配置器
本质上其实就是glibc中的malloc 和 free.
直接使用OK吗? OK, 内存直接交给用户去向系统申请。是否存在问题, 存在, 用户申请的内存很可能是没有规律的小块内存. 大量的小块内存的申请会带来内存碎片. 导致大内存的申请失败.
如下的_malloc_alloc_template就是allocator (T )追溯到最后的一级空间配置器类
一级空间配置器函数细节分析
//SGI Allocator相较于operator new的优势所在一.
static void* _S_oom_malloc(size_t);
static void* _S_oom_realloc(void*, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
//oom: out of memory 内存溢出, 内存不足的意思.
SGI SIL 多出来的_S_oom_malloc + _S_oom_realloc 功能是干啥? 往下追溯一下
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
void (* __my_malloc_handler)();//malloc_handler: 回调处理函数
void* __result;
/*
while (set alloc_oom_handler)
循环不断的执行handler
直至 malloc sucess.
if (not set alloc_oom_handler)
throw bad_alloc();
*/
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);
}
}
其实刨析一个函数足以, Allocator 一级空间配置器相较于全局 operator new 的优势所在...
-
上述优势依然明了, 就是可以在系统内存不足OOM的 情况下询问是否设置了内存释放的callback. 此callback必须保证能够释放出有效内存. 也就是我们确保为整个程序的正常运行设置的预留可free 内存. 否则恐存在陷入无穷无尽死循环的风险.
二级空间配置器
抓主干, 先看懂重要的成员函数 + 数据成员
#if ! (defined(__SUNPRO_CC) || defined(__GNUC__))
enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN
???? 啥意思??? 三个enum给我甩出来了
-
ALIGN: 对齐, 排齐, 排列的意思. 其实就是对齐字节数. 所分配的内存一定是ALIGN的整数倍. 内存分配按照ALIGN对齐
-
—MAX_BYTES最大字节数, 可分配的最大字节数. 内存池上限
-
NFREELISTS = (MAX_BYTES / _ALIGN) 自由链表个数后序细说含义
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this.*/
};
//静态数据成员, _S_free_list
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
要理解上述元素结构定义+成员定义. 我们必须对于union联合体有着一定的认知
-
union: 一段内存, 在不同的场景有着不同的含义, 存储不同的身份. 但是公用一块内存空间. 有点像人格分裂. 在不同的特殊场景下采用不同的人格, 但是本质上都是一个存储空间.
-
上述的_Obj定义非常之巧妙. 占 4 byte空间大小. 充当不同身份. 内存池中充当自由链表头部list_link. 在分配出去的时候充当client_data. 数据空间
-
下面这副图基本就是整个SGI STL内存池的核心所在. 后序会更加细节讲述为啥要这样设计, 为啥一定要采用free_list, 而不是直接用动态链表. 而且SGI STL牛逼之处还远不止下面这一点
//上调取ALIGN整数倍函数
static size_t
_S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }
//Obj* 数组下标定位函数
static size_t _S_freelist_index(size_t __bytes) {
return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);
}
-
如何理解上述两个函数, 可谓是经常在源代码中见到. 特别是round_up. 用于字节对齐, 特常见.
-
(size_t) 是强制转换为4 bytes.
-
((size_t)_ALIGN - 1) 8 - 1 = 7 = 0000 0000 0000 0000 0000 0000 0000 0111
-
~((size_t)_ALIGN - 1) = 1111 1111 1111 1111 1111 1111 1111 1000
-
any number & ~((size_t)_ALIGN - 1) 必然只剩下了 _ALIGN二进制整数倍位之前的位了. 后面的非整数倍低位全部&0 = 0 了
-
eg:8 & ~((size_t)ALIGN - 1) = 8 11 & ~((size_t)ALIGN - 1) = 8 16 & ~((size_t)_ALIGN - 1) = 16
-
我们还需要round_up向上取整数倍. 所以左边仍需要 (__bytes) + (size_t) _ALIGN-1操作
上述思路同理可以推到index函数
static char* _S_start_free; //当前空闲内存起点
static char* _S_end_free; //空闲内存末尾
static size_t _S_heap_size; //已利用内存, 用于make free_list的内存
//上述静态成员数据的初始化
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_start_free = 0;
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_end_free = 0;
template <bool __threads, int __inst>
size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0;
allocate(int size); 内存分配函数
/* __n must be > 0 */
static void* allocate(size_t __n)
{
void* __ret = 0;
if (__n > (size_t) _MAX_BYTES) { //超过内存池最大限制
__ret = malloc_alloc::allocate(__n);//采取malloc
}
else {//从内存池中获取内存
_Obj* volatile* __my_free_list
= _S_free_list + _S_freelist_index(__n); //指向对应chunk块所在自由链表表头
# ifndef _NOTHREADS
_Lock __lock_instance; //智能锁. 线程安全, 操作内存池(全局资源, 多线程共享)
# endif
//尝试获取头部chunk块
_Obj* __RESTRICT __result = *__my_free_list;//拿取头部
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));//填充chunk块free_list并return head chunk块
else {//chunk块free_list not null 拿走头部chunk块, 并且进行pop_front
*__my_free_list = __result -> _M_free_list_link;
__ret = __result;
}
}
return __ret;
};
在此之前, 先来点指针级别基础, 不然怕理解不了.
-
_Obj* 作为数组元素, 相当于数组中元素是一级指针类型
-
所以遍历数组的指针应该是指向_Obj*类型的指针, 也就应该是 _Obj** 类型的指针
-
上述还不好理解的话,想一想int 数组遍历的指针为int* . int* 数组的遍历指针肯定应该是int **. so 上述同理。
-
Obj** 的指针进行解引用, 其中存储的就是 _Obj* 也就是第一个chunk块. _free_list的 head node
-
volatile 禁止编译器优化.
简单画图理解一下下.
接下来看的就是_S_refill() 究竟是如何创建并且填充 chunk_free_list的
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20; //数目. chunk块数目
char* __chunk = _S_chunk_alloc(__n, __nobjs);//alloc 先分配内存. 貌似就是一维数组
_Obj* volatile* __my_free_list;
_Obj* __result;
_Obj* __current_obj; //当前对象, 也就是记录当前chunk块
_Obj* __next_obj; //下一个chunk块
int __i;
if (1 == __nobjs) return(__chunk);//只有一个chunk块, 直接return
/*
往下必然是超过一个chunk块了, 干啥??? 不可以直接return ???
上述仅仅create memory了. 我们需要的是链表, _free_list. so: 必然是分割memory为chunk块并且link起来
*/
__my_free_list = _S_free_list + _S_freelist_index(__n);//拿到对应chunk块head node
/* Build free list in chunk */
__result = (_Obj*)__chunk;// get ans chunk
*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);//pop_front
for (__i = 1; ; __i++) {//link 操作n 个node link n-1次 + pop_front, so link objs-2次
__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);
}
/*
按照我上面这样的分析策略, 我猜,接下来你肯定能知道我要分析哪一个函数了. 对的没错,就是它:
chunk_alloc. chunk_list的内存究竟如何分配的. 如何能做到内存的最大利用, 最少的碎片。 精华在此处
*/
/*我滴妈呀. 真的好大. 有这么麻烦么?? 直接分配objs个chunk块 return 不就OK了么, 错,那是我们这种寻常人的想法, 但是写库的能是凡人嘛. 采取一种每次申请2倍甚至更多的memory. 申请的memory size 随着我们已经申请的memory数目变大而变大
好处何在?
竟然已经申请的内存越来越大,说明向内存池申请内存太频繁了. 我们就一次给多一点, 避免不停的需要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;//剩余空闲内存量
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);
} 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);
} else {
/*
各位大爷, 我们先不看上面那两种特殊情况, 那也不是人想出来的.
我们先看下面正常情况. 正常来讲肯定是内存不足. bytes_left < _total_bytes.
我们才走到了再次alloc的情景下. 只要有内存,谁没事去alloc, 系统调用不要钱是吧
*/
size_t __bytes_to_get =
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
// Try to make use of the left-over piece.
if (__bytes_left > 0) {
_Obj* __STL_VOLATILE* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
_S_start_free = (char*)malloc(__bytes_to_get);
if (0 == _S_start_free) {
size_t __i;
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __p;
// Try to make do with what we have. That can't
// hurt. We do not try smaller requests, since that tends
// to result in disaster on multi-process machines.
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;
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));
// Any leftover piece will eventually make it to the
// right free list.
}
}
_S_end_free = 0; // In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
// This should either throw an
// exception or remedy the situation. Thus we assume it
// succeeded.
}
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;
return(_S_chunk_alloc(__size, __nobjs));
}
}
如下才是真正需要从系统malloc的主干code. if (malloc()==0)的特殊处理, 暂时被移除出去了, 先研究正常的逻辑, 先抓住功能核心. 再到细节.
size_t __bytes_to_get = //need malloc(__bytes_to_get) 用于 make _memory_free_list
2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
//之前剩余的空闲内存__bytes_left > 0. 我们扔掉? NO 想办法利用起来
if (__bytes_left > 0) {
_Obj* volatile* __my_free_list =
_S_free_list + _S_freelist_index(__bytes_left);
//头部插入到可以插入的小chunk块. 将剩余的small memory利用起来
((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
*__my_free_list = (_Obj*)_S_start_free;
}
//为内存池扩充内存. 产生 _chunk_free_list to provide chunk for user
_S_start_free = (char*)malloc(__bytes_to_get);
_S_heap_size += __bytes_to_get;//已利用的内存增加,制作成chunk_free_list的内存部分
_S_end_free = _S_start_free + __bytes_to_get;//跳过已用于制作chunk_free_list内存
return(_S_chunk_alloc(__size, __nobjs));//递归调用, 精髓在于前面的两个if
//分析一下if(malloc()==0)的处理模块
if (0 == _S_start_free) {
size_t __i;
_Obj* volatile* __my_free_list;
_Obj* __p;
/*
如下做出的努力是什么嘛, 说白了,malloc失败, 应该是oof了. 系统内存不足. 所以如下做出的努力是在其他的_chunk_free_list寻找希望. 希望找这些已利用内存,哥们. 我实在坚持不住了, 你看看能借我一个chunk块不. 但是这个借了,我也没能力偿还. 相当于强者拉一把弱者
*/
for (__i = __size;//从need one chunk memory size 之后的_chunk_free_list做出努力
__i <= (size_t) _MAX_BYTES;
__i += (size_t) _ALIGN) {
__my_free_list = _S_free_list + _S_freelist_index(__i);
__p = *__my_free_list;
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));
}
}
//走到此处. 真的是弹尽粮绝了, 兄弟们都饿呀.
//你还是再去系统仓库撞撞运气吧.
_S_end_free = 0; // In case of exception.
_S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);
}
至于此处, 我们在研究研究前面两个if究竟是干嘛的
//内存够了, 咋可能突然足够make chunk_free_list,
//必然是之前走了malloc. 对内存池扩充了可用内存
if (__bytes_left >= __total_bytes) {
__result = _S_start_free;
_S_start_free += __total_bytes;
return(__result);//真正的return memory逻辑. 将搞内存和return 内存分开
}
//下面这个是个狠人逻辑. 虽然不足以整个__total_bytes 的分配, 但是我暂时还不想去malloc. 内存物尽其用. 能分配几个chunk块, 就利用起来产生几个chunk块咯. 总不能丢掉这点直接去从新malloc内存池吧.
//牛逼之处, 真的做到了一丁点不浪费, 边角料大不了少做几个
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);
}
eg: 上述逻辑是分析完了, 但是貌似都是写干说, 真正要想理解深刻其实还是画图借助思考
-
eg:我首先申请第一个7 bytes 内存.
-
然后申请15 bytes内存
基本剧终了
-
另外一个比较重要的函数就是deallocate. 但是我觉得至此,聪明的各位绝对知晓了,deallocate该干啥, 没错就是将用完的chunk块归还到chunk_free_list待其他人使用
-
你可能想说,还有这么多加锁,确实,对于内存池整个全局资源的操作可能是多线程的, 所以需要加上一个局部对象的智能锁, 他这里写的牛逼, 其实本质还是RAII技术
static void deallocate(void* __p, size_t __n)
{
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n);
else {
_Obj* volatile* __my_free_list
= _S_free_list + _S_freelist_index(__n);
_Obj* __q = (_Obj*)__p;
// acquire lock
# ifndef _NOTHREADS
/*REFERENCED*/
_Lock __lock_instance;
# endif /* _NOTHREADS */
__q -> _M_free_list_link = *__my_free_list;
*__my_free_list = __q;
// lock is released here
}
}
-
至此,小杰仅以粗俗己见, 跟大家分享一些技术上学习的技巧, 从主干到细节, 从框架到细节, 一定要真的抓住框架, 少一些跟着别人写. 不然那样自己会成长的很慢
-
重要的小模块也要多写, 我们cpp程序员确实少不了造轮子的命, 这多少枯燥了些, 但是想想以后一个好的工作, 生活的压力少一些还是好的. 单独的轮子都会造之后,最好的多看看一些框架, github上的一些知名项目. 虽然这样看,短期貌似你的代码量提升不大, 但是长期来看,这样是真正对自己整体实力提升最大的