目录
- 总览
- 全局对象构建析构
- 全局区间对象fill/copy
- 双顶层内存缓冲器
- 示例
- reference
总览
大体stg-stl分为alloctor, iter, adapter, container, algorithms, functions
原图来自note/STL源码剖析.md at master · arkingc/note · GitHub
alloctor把对象内存申请&构造拆分开来
- stl_construct.h 全局对象的构建(ctor), 析构(dector), 对其stl接口标准
- stl_uninitialized.h 全局对象的赋值, 初始化等, 这部分也是对其stl接口标准
- stl_alloc.h 全局双顶层对象缓冲
全局对象构建析构(stl_construct.h)
这里函数_XXX(比如_Construct)都是stg内置的函数, 下面这种是对其stl标准接口
// --------------------------------------------------
// Old names from the HP STL.
template
inline void construct(_T1* __p, const _T2& __value)
这里以construct为线看一下, 这里有两个特点:
- 构建对象一般是: [内存分配] -> [构造器], 这里使用placement new算子, 仅仅调用构造器, 这样object(s)的内存和ctor就解耦了
- 区分对待trival对象, 因为trival对象可以使用更加高效的构建方式.
简单的结构如下:
construct(_T1* __p, const _T2& __value)
-> _Construct(_T1* __p, const _T2& __value) { new ((void*) __p) _T1(__value); }
construct(_T1* __p)
-> void _Construct(_T1* __p) { new ((void*) __p) _T1(); }
destroy(_ForwardIterator __first, _ForwardIterator __last)
-> _Destroy(_ForwardIterator __first, _ForwardIterator __last)
-> __destroy(_ForwardIterator __first, _ForwardIterator __last, _Tp*) 萃取当前类型是否有析构
有 -> __destroy_aux(_ForwardIterator __first, _ForwardIterator __last, __false_type) for-each destroy(&*__first);
无 -> 啥都不干, 这一步只是为了析构, trival无需特殊析构
在看过上面例子够, 这里引申两点:
- c++本身是静态语言, 不能运行时获取object的meta info, 所以萃取其实是一个编写上的契约, 比如当前以迭代器(iter)方式构建或者删除时, 从迭代器可以获取value类型, 从value类型就可以知道是否是trivial类型, 假如自定义的iter没有按照契约定义是否trival类型, 编译时内联生成代码时就不能找到属性
- 实际调用时通过重载来实现, 不存在运行时的开销(struct __true_type{}, struct __false_type)
- trivial类型不走ctor, copy, assign, dector, move直接使用memcpy, memmove等高效方式
- 如何界定object是trivial, 满足以下就是none trivial否则就是trivial
a. 显示定义构造函数(ctor), 复制构造函数(copy), 移动构造(move), 赋值运算符(assign), 析构函数(dector)
b. 类型有非POD(plain old data)类型成员
c. 有虚函数, 有基类
POD类型如下:
- 算数类型
- enum
- pointer(nullptr/object pointer/function pointer)
- 到类成员的指针类型 比如C类, M成员, C::M*
- pod类型组成的 class, struct, union
全局区间对象fill/copy (stl_uninitialized.h)
这里也是萃取了iter中是否为pod的类型优化
uninitialized_copy(_InputIter __first, _InputIter __last,
_ForwardIter __result) (萃取value pointer type)
-> __uninitialized_copy(_InputIter __first, _InputIter __last,
_ForwardIter __result, _Tp*) (从Tp萃取is pod)
-> 是pod , 调用algo_base的 _OutputIter copy(_InputIter __first, _InputIter __last, _OutputIter __result)
-> 不是pod foreach调用stl_contruct中的构建方法 for(***) _Construct(&*__cur, *__first);
uninitialized_fill, uninitialize_fill_n等等都是这个思路
双顶层内存缓冲器
这里stg没有使用stl标准的std::allocator, 而是使用内部实现的高效std::alloc, 这里并没有严格接口对其.
这里定义了内部接口供内部容器使用:
template
class simple_alloc {
public:
static _Tp* allocate(size_t __n)
{ return 0 == __n ? 0 : (_Tp*) _Alloc::allocate(__n * sizeof (_Tp)); }
static _Tp* allocate(void)
{ return (_Tp*) _Alloc::allocate(sizeof (_Tp)); }
static void deallocate(_Tp* __p, size_t __n)
{ if (0 != __n) _Alloc::deallocate(__p, __n * sizeof (_Tp)); }
static void deallocate(_Tp* __p)
{ _Alloc::deallocate(__p, sizeof (_Tp)); }
};
具体实现__malloc_alloc_template和__default_alloc_template
其中__malloc_alloc_template没啥东西只是对malloc的简单封装, 可以设置oom时的回调函数
以下重点分析__default_alloc_template.
设计要义&方法
- 解决什么问题
在glibc中其实也是一样的问题和思路, 针对client零乱大量的申请/释放, 会形成一些列大小不一且不相连的使用内存片段, 会产生出内碎片的问题 - slot化
和内存分页一个思路, 把memory分成不同chunk slot, 把请求离散标准化, 这样就形成了一系列的slot, [8, 16, 24, 32, 40, ..., 120, 128] 当需要5 byte时, 向上对其使用8 byte的slot分配, 这样slot内的内碎片均值为4. 但是slot是有边界的, 当申请>128 byte时直接使用__malloc_alloc_template, __default_alloc_template旨在解决小内存大量零碎调用 - 全局缓充
为了避免连续不同slot的调用还设置的全集缓存, 当前slot不够的直接从全局缓存出 -
释放缓存
释放缓存时实际是归还给了stg的内存缓存池, 以便后续调用时再次申请
图示如下:
代码详情
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
}; //union 用户视角char*, 系统视角满足当前slot长度, 首地址指向一个slot chunk地址
static _Obj* __STL_VOLATILE _S_free_list[] //全局slot, 每一个元素指向slot链表
static void* allocate(size_t __n)
{
void* __ret = 0;
if (__n > (size_t) _MAX_BYTES) {
__ret = malloc_alloc::allocate(__n); // -> __malloc_alloc_template
}
else {
_Obj* __STL_VOLATILE* __my_free_list
= _S_free_list + _S_freelist_index(__n); //_S_freelist_index(__n)依据申请找到合适slot
// 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;
if (__result == 0)
__ret = _S_refill(_S_round_up(__n));//申请内存
else {
*__my_free_list = __result -> _M_free_list_link; //以前以后缓存, 拔出第一片返回, 回写下一原有free到_S_free_list[x](*__my_free_list <-)
__ret = __result;
}
}
return __ret;
};
同样deallocate把归还内存按照slot插入到起始
static void deallocate(void* __p, size_t __n)
{
if (__n > (size_t) _MAX_BYTES)
malloc_alloc::deallocate(__p, __n); // free
else {
_Obj* __STL_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; //回写_S_free_list对应slot
// lock is released here
}
}
重点是_S_refill和_S_chunk_alloc, 只要调入_S_refill一定是内存当前slot内存不够了
_S_refill调用_S_chunk_alloc分配足量内存, 串联slot内链表结构.
_S_chunk_alloc 分配足够了内存, 具体如何足量下面有解释
/* Returns an object of size __n, and optionally adds to size __n free list.*/
/* We assume that __n is properly aligned. */
/* We hold the allocation lock. */
template
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
int __nobjs = 20;
char* __chunk = _S_chunk_alloc(__n, __nobjs); //调用一个__n size chunk, 可以返回多个, 返回内存划分:[__chunk, __chunk + __n*nobjs][free_start, free_end]
_Obj* __STL_VOLATILE* __my_free_list;
_Obj* __result;
_Obj* __current_obj;
_Obj* __next_obj;
int __i;
if (1 == __nobjs) return(__chunk);
__my_free_list = _S_free_list + _S_freelist_index(__n);//只够一个就地返回
/* Build free list in chunk */
__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的责任是分配给足量的内存, 把内存切分一部分给slot缓存, 一部分全局缓存.
client需求一个__n的内存, 足量体现在:
- _S_chunk_alloc看全局缓存余量是否够20__n, 如果够一次划给slot20__n
- _S_chunk_alloc看全局缓存余量[1__n, 不足20__n], 一次划给最大的最大数目__n大小
- _S_chunk_alloc按照需求内存220__n + left_space >> 4, 把20*__n划给slot, 这个体现在返回__nobjs和_S_start_free的设置, 这部分代码相对多一些
template
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); //够20
} 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); //够至少一个, 有多少给多少slot
} else {
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) { //把余下全局缓冲区挂入__bytes_left缓冲区
_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; //_S_start_free当slot接到_S_free_list中
*__my_free_list = (_Obj*)_S_start_free; //回写_S_free_list
}
_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; //在上面之前全局缓存已经挂入slot
_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;//整块[_S_start_free, _S_end_free] 下载递归调用, 划分slot
return(_S_chunk_alloc(__size, __nobjs));
}
}
调用示例
假如slot目前为空
- call a(6)
slot 8 | 16 | ... | 128 | 全局 |
---|---|---|---|---|
8*19 8 -> ... -> 8 -> ^ | ^ | ^ | ^ | 20*8 |
- call a(128)
slot 8 | 16 | ... | 128 | 全局 |
---|---|---|---|---|
8*19 8 -> ... -> 8 -> ^ | ^ | ^ | ^ | 32 = 20*32 - 128 |
- call a(128)
slot 8 | 16 | 32 | ... | 128 | 全局 |
---|---|---|---|---|---|
8*19 8 -> ... -> 8 -> ^ | ^ | 32->^ | 128*19 128 -> ... -> 128 -> ^ | 20*128 |
- call d(8)
slot 8 | 16 | 32 | ... | 128 | 全局 |
---|---|---|---|---|---|
8*20 8 -> ... -> 8 -> ^ | ^ | 32->^ | 128*19 128 -> ... -> 128 -> ^ | 20*128 |
有此看出:
- 专注小内存分配, 大内存交给malloc
- 把小内存划分离散的slot, 有点像一些列的齿轮, 齿比是8字节间隔, 看需求咬合那个最合适
- slot要一次申请多个, 能够就给slot缓存, 这样避免了多次小内存申请
- 再设置一级全局缓存, 补充空的slot内存需求
- 当内存不足时考虑了, 把全局内存归并slot, 利用现有slot化解, 如果不能化解报错
- 当free内存时, 不是真正free, 按照slot放回, 减少系统调用, 下次再需要直接返回
- slot 存储overhead为0, 使用union, 系统视角一个指针指向next free slot, 用户视角为未初始化内存
示例
下面以常用vector示例看下如何使用内存分配机制
vector代码片段如下:
template
class vector : protected _Vector_base<_Tp, _Alloc>
{
// requirements:
__STL_CLASS_REQUIRES(_Tp, _Assignable);
private:
typedef _Vector_base<_Tp, _Alloc> _Base;
define __STL_DEFAULT_ALLOCATOR(T) alloc // 配置在stl_config.h
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc; //默认是没有__USE_MALLOC, alloc使用上述__default_alloc_template分配
//vector base代码片段
template //__default_alloc_template
class _Vector_base {
public:
typedef _Alloc allocator_type; //__default_alloc_template
allocator_type get_allocator() const { return allocator_type(); }
_Vector_base(const _Alloc&)
: _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
_Vector_base(size_t __n, const _Alloc&)
: _M_start(0), _M_finish(0), _M_end_of_storage(0)
{
_M_start = _M_allocate(__n);
_M_finish = _M_start;
_M_end_of_storage = _M_start + __n;
}
~_Vector_base() { _M_deallocate(_M_start, _M_end_of_storage - _M_start); }
typedef simple_alloc<_Tp, _Alloc> _M_data_allocator; // 用上述simple_alloc做接口, 传入__default_alloc_template实现类型
_Tp* _M_allocate(size_t __n)
{ return _M_data_allocator::allocate(__n); } //调入内存分配
假如有如下代码
auto v = vector(3,0);
for (auto i = 4; i < 8; ++i){
emplace_v = vector(i, 0);
}
v申请43(size(int)v.size()) 12 bytes, 归并申请到16字节
slot 8 | 16 | ... | ^ | 全局 |
---|---|---|---|---|
^ | 19 free list | ... | ^ | 12*20 |
emplace_v(i = 4)申请4*4后, 直接使用free list
slot 8 | 16 | ... | 128 | 全局 |
---|---|---|---|---|
^ | 18 free list | ... | ^ | 12*20 |
emplace_v释放后, 插入slot 16首位
slot 8 | 16 | ... | 128 | 全局 |
---|---|---|---|---|
^ | 19 free list | ... | ^ | 12*20 |
emplace_v(i = 5)申请4*5后, 使用slot 24, 从全局free分配, 分配10个
slot 8 | 16 | 24 | ... | ^ | 全局 |
---|---|---|---|---|---|
^ | 19 free list | 9 free list | ... | ^ | ^ |
emplace_v释放后 slot 16归还
slot 8 | 16 | 24 | ... | ^ | 全局 |
---|---|---|---|---|---|
^ | 19 free list | 10 free list | ... | ^ | ^ |
emplace_v (i = 6)申请46和上一轮情况相同
emplace_v (i = 7)申请47, 使用slot 30, 全局为空, 申请
slot 8 | 16 | 24 | 30+... | ^ | 全局 | |
---|---|---|---|---|---|---|
^ | 19 free list | 10 free list | 19 free list | ... | ^ | 20*30 |
emplace_v释放4*7
slot 8 | 16 | 24 | 30+... | ^ | 全局 | |
---|---|---|---|---|---|---|
^ | 19 free list | 10 free list | 20 free list | ... | ^ | 20*30 |
reference:
https://github.com/arkingc/note/blob/master/C%2B%2B/STL%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90.md#4stl%E5%85%AD%E5%A4%A7%E9%83%A8%E4%BB%B6
https://backendhouse.github.io/post/stl%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-traits/
https://stackoverflow.com/questions/51659101/why-can-static-data-member-not-be-initialized-in-class-in-c11