STL空间分配器源码分析(一)
一、摘要
STL的空间分配器(allocator)定义于命名空间std内,主要为STL容器提供内存的分配和释放、对象的构造和析构的统一管理。空间分配器的实现细节,对于容器来说完全透明,容器不需关注内存分配和回收的策略细节如何。
STL allocator需实现如下4个标准接口
pointer allocate(size_type __n, const void*); //内存分配
void deallocate(pointer __p, size_type __n); //内存释放
void construct(pointer __p, const _Tp& __val);//构造
void destroy(pointer __p); //析构
二、STL的几种空间分配器介绍
- __new_allocator:C++标准中定义的分配器,仅对operator new和operator delete做简单封装
- malloc_allocator:C++标准中定义的分配器,仅对std::malloc和std::free做简单封装
- __mt_alloc:一种支持多线程的空间配置器(亦可单线程),可分配2的幂次方大小的内存块,该配置器可灵活调整,性能高(stl手册描述,个人未实测)
- bitmap_allocator:一种使用位图来区分内存是否分配的配置器
- __pool_alloc:带有单锁内存池的器,即侯捷于《STL源码剖析》中介绍的SGI-STL空间配置器。
- debug_allocator:该空间分配器主要用于调试,可包裹其他allocator,于用户层申请的内存大小的基础上扩容部分,附带调试信息。
- throw_allocator:具有日志记录和异常生成控制的分配器
补充说明:operator new和std::malloc都是仅申请内存,申请的内存不做初始化,但其仍存在如下区别:
- operator new 可由用户重载,调用new关键字时将自动调用重载的operator new函数,而std::malloc不能重载
- operator new 有异常机制,在内存申请失败时会抛出异常,std::malloc申请失败只会返回NULL
- std::malloc 可和realloc结合使用,调整内存申请大小,operator new无类似的操作
三、new_allocator
template<typename _Tp>
class __new_allocator
{
public:
/* 重定义了几种类型别名 */
typedef _Tp value_type;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
#if __cplusplus <= 201703L
typedef _Tp* pointer;
typedef const _Tp* const_pointer;
typedef _Tp& reference;
typedef const _Tp& const_reference; template<typename _Tp1>
struct rebind
{ typedef __new_allocator<_Tp1> other; };
#endif /* 以下几个构造和析构函数,没有做特别实现 */
_GLIBCXX20_CONSTEXPR
__new_allocator() _GLIBCXX_USE_NOEXCEPT { } _GLIBCXX20_CONSTEXPR
__new_allocator(const __new_allocator&) _GLIBCXX_USE_NOEXCEPT { } template<typename _Tp1>
_GLIBCXX20_CONSTEXPR
__new_allocator(const __new_allocator<_Tp1>&) _GLIBCXX_USE_NOEXCEPT { } #if __cplusplus <= 201703L
~__new_allocator() _GLIBCXX_USE_NOEXCEPT { } /* 取参数地址 */
pointer
address(reference __x) const _GLIBCXX_NOEXCEPT
{ return std::__addressof(__x); } const_pointer
address(const_reference __x) const _GLIBCXX_NOEXCEPT
{ return std::__addressof(__x); }
#endif _GLIBCXX_NODISCARD _Tp*
allocate(size_type __n, const void* = static_cast<const void*>(0)); void
deallocate(_Tp* __p, size_type __n __attribute__ ((__unused__))); #if __cplusplus <= 201703L
size_type
max_size() const _GLIBCXX_USE_NOEXCEPT
{ return _M_max_size(); } #if __cplusplus >= 201103L
/* C++11新特性,采用std::forward将参数完美转发,保留其右值属性
* 调用placement new,在已分配好的内存上构造对象,形参为std::forward转发的参数
*/
template<typename _Up, typename... _Args>
void
construct(_Up* __p, _Args&&... __args)
noexcept(std::is_nothrow_constructible<_Up, _Args...>::value)
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); } /* 显示调用析构函数 */
template<typename _Up>
void
destroy(_Up* __p)
noexcept(std::is_nothrow_destructible<_Up>::value)
{ __p->~_Up(); }
#else
/* 调用placement new,在已分配好的内存上构造对象 */
void
construct(pointer __p, const _Tp& __val)
{ ::new((void *)__p) _Tp(__val); } /* 显示调用析构函数 */
void
destroy(pointer __p) { __p->~_Tp(); }
#endif
#endif // ! C++20 /* 以下重载了“=”和“!=”运算符,不做特殊实现 */
template<typename _Up>
friend _GLIBCXX20_CONSTEXPR bool
operator==(const __new_allocator&, const __new_allocator<_Up>&)
_GLIBCXX_NOTHROW
{ return true; } #if __cpp_impl_three_way_comparison < 201907L
template<typename _Up>
friend _GLIBCXX20_CONSTEXPR bool
operator!=(const __new_allocator&, const __new_allocator<_Up>&)
_GLIBCXX_NOTHROW
{ return false; }
#endif private:
_GLIBCXX_CONSTEXPR size_type
_M_max_size() const _GLIBCXX_USE_NOEXCEPT;
}
以上代码裁剪了源码的部分实现,加入个人注释理解。空间配置器的重点在于内存的分配和释放,对象的构造和析构,new_allocator对此仅用operator new、operator delete、placement new做简单封装。
上述代码中rebind 的定义是stl中的一个特点,所有的空间配置器都实现了类似如下的定义
template<typename _Tp1>
struct rebind
{ typedef __new_allocator<_Tp1> other; };
其作用在于,实现对不同类型采用同一内存分配策略的需求。
空间配置器作为容器的模板参数,容器只知其形参名而不知其具体的内存配置策略如何,当容器需要对另一类型采用同样的内存配置策略时,此时就可以采用rebind,获取到其所需另一类型的,符合同一内存分配策略的空间配置器
std::allcoator<T>::rebind<U>::other等价于std::allcoator<U>。
allocate的实现如下
template<typename _Tp1>
struct rebind
{ typedef __new_allocator<_Tp1> other; }; _GLIBCXX_NODISCARD _Tp*
allocate(size_type __n, const void* = static_cast<const void*>(0))
{
#if __cplusplus >= 201103L
/* 静态断言,编译期间检查类型大小 */
static_assert(sizeof(_Tp) != 0, "cannot allocate incomplete types");
#endif /* GCC 提供的分支预测 */
if (__builtin_expect(__n > this->_M_max_size(), false))
{
if (__n > (std::size_t(-1) / sizeof(_Tp)))
std::__throw_bad_array_new_length();
std::__throw_bad_alloc();
} #if __cpp_aligned_new
/* 当类型对齐后的大小大于系统默认内存对齐大小,采用
* void* operator new ( std::size_t count, std::align_val_t al) 作为内存申请的接口
*/
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
std::align_val_t __al = std::align_val_t(alignof(_Tp));
return static_cast<_Tp*>(_GLIBCXX_OPERATOR_NEW(__n * sizeof(_Tp),
__al));
}
#endif
return static_cast<_Tp*>(_GLIBCXX_OPERATOR_NEW(__n * sizeof(_Tp)));
}
- __builtin_expect(exp, x) 是GCC的一个内建函数,用于分支预测,提高性能(处理if else分支时,前面分支的汇编指令会先装载,后面分支的指令需要通过JMP指令才能访问,JMP访问比前者更耗时间,大量的JMP访问会有性能开销,因此,采用分支预测,CPU提前装载执行概率更高的指令,提高性能)。
__builtin_expect(exp, x)期望的表达式exp==x,当x=0时,if分支执行的可能性小,否则else分支执行的可能性小。函数的范围值为exp。
- __STDCPP_DEFAULT_NEW_ALIGNMENT__ 是 operator new 操作对齐值的阈值,超过这个值,operator new将无法保证分配的内存满足对齐要求,此时可用 void* operator new ( std::size_t count, std::align_val_t al) 作为内存申请的接口,该接口为C++17实现。接口将强行使用指定的参数作为内存对齐的大小分配内存。
- alignof(_Tp)运算符用于计算类型的内存对齐大小。std::align_val_t 为枚举类型,定义如:enum class align_val_t: size_t {}; 域化枚举,并指定类型为size_t。
deallocate的实现如下
void
deallocate(_Tp* __p, size_type __n __attribute__ ((__unused__)))
{
#if __cpp_sized_deallocation
# define _GLIBCXX_SIZED_DEALLOC(p, n) (p), (n) * sizeof(_Tp)
#else
# define _GLIBCXX_SIZED_DEALLOC(p, n) (p)
#endif #if __cpp_aligned_new
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
_GLIBCXX_OPERATOR_DELETE(_GLIBCXX_SIZED_DEALLOC(__p, __n),
std::align_val_t(alignof(_Tp)));
return;
}
#endif
_GLIBCXX_OPERATOR_DELETE(_GLIBCXX_SIZED_DEALLOC(__p, __n));
}
其中几个宏的定义如下
#if __has_builtin(__builtin_operator_new) >= 201802L
# define _GLIBCXX_OPERATOR_NEW __builtin_operator_new
# define _GLIBCXX_OPERATOR_DELETE __builtin_operator_delete
#else
# define _GLIBCXX_OPERATOR_NEW ::operator new
# define _GLIBCXX_OPERATOR_DELETE ::operator delete
#endif
_M_max_size的实现如下
_GLIBCXX_CONSTEXPR size_type
_M_max_size() const _GLIBCXX_USE_NOEXCEPT
{
#if __PTRDIFF_MAX__ < __SIZE_MAX__
return std::size_t(__PTRDIFF_MAX__) / sizeof(_Tp);
#else
return std::size_t(-1) / sizeof(_Tp);
#endif
}
四、malloc_allocator
malloc_allocator的实现与new allocator实现类似,区别在于调用的接口不同,malloc_allocator封装的接口为std::malloc和std::free。此空间配置器不做源码分析。
本系列章节所分析的源码基于gcc-master,源码路径mirrors / gcc-mirror / gcc · GitCode。博客内容仅做学习记录分享。
STL空间分配器源码分析(一)的更多相关文章
- STL空间分配器源码分析(二)mt_allocator
一.简介 mt allocator 是一种以2的幂次方字节大小为分配单位的空间配置器,支持多线程和单线程.该配置器灵活可调,性能高. 分配器有三个通用组件:一个描述内存池特性的数据,一个包含该池的策略 ...
- STL空间分配器源码分析(三)pool_allocator
一.摘要 pool_allocator是一种基于单锁内存池的空间分配器,其内部采用内存池思想,通过构建16个空闲内存块队列,来进行内存的申请和回收处理.每个空闲队列管理的内存块大小固定,且均为8的倍数 ...
- STL源码分析读书笔记--第二章--空间配置器(allocator)
声明:侯捷先生的STL源码剖析第二章个人感觉讲得蛮乱的,而且跟第三章有关,建议看完第三章再看第二章,网上有人上传了一篇读书笔记,觉得这个读书笔记的内容和编排还不错,我的这篇总结基本就延续了该读书笔记的 ...
- STL源码分析《3》----辅助空间不足时,如何进行归并排序
两个连在一起的序列 [first, middle) 和 [middle, last) 都已经排序, 归并排序最核心的算法就是 将 [first, middle) 和 [middle, last) 在 ...
- STL空间配置器源码分析(四)bitmap_allocator
一.摘要 bitmap_allocator是STL空间分配器的其中一种,它采用内存池策略,最多存储64条空闲链表(freelist,实际是一块空间连续的内存区,后面也称为超级块),每条空闲链表存储的内 ...
- stl源码分析之vector
上篇简单介绍了gcc4.8提供的几种allocator的实现方法和作用,这是所有stl组件的基础,容器必须通过allocator申请分配内存和释放内存,至于底层是直接分配释放内存还是使用内存池等方法就 ...
- linux内存源码分析 - SLAB分配器概述【转】
本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 之前说了管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理内存的页框分配给申请 ...
- linux内存源码分析 - SLAB分配器概述
本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 之前说了管理区页框分配器,这里我们简称为页框分配器,在页框分配器中主要是管理物理内存,将物理内存的页框分配给申请 ...
- stl源码分析之list
本文主要分析gcc4.8版本的stl list的源码实现,与vector的线性空间结构不同,list的节点是任意分散的,节点之间通过指针连接,好处是在任何位置插入删除元素都只需要常数时间,缺点是不能随 ...
随机推荐
- MATLAB绘制三角网及三维网线
今天博主给大家介绍一些比较常见的可视化操作,绘制三角网及三维网线. 三角网是由一系列连续三角形构成的网状的平面控制图形,是三角测量中布设连续三角形的两种主要扩展形式,同时向各方向扩展而构成网状,优点为 ...
- Ubuntu18.04..5 配置国内镜像源:解决E: Failed to fetch
镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 问题描述 使用 sudo apt get-install 出现 E: Failed to fetch问题. 更换镜像源 错误原因:绝大多数情况下, ...
- 【基础】java环境搭建及配置--->【关注微信公众号:三叔测试笔记,及时获取干货】
一.下载安装 Java官网下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.ht ...
- 在JVM的新生代内存中,为什么除了Eden区,还要设置两个Survivor区
在JVM的新生代内存中,为什么除了Eden区,还要设置两个Survivor区? 1 为什么要有Survivor区 先不去想为什么有两个Survivor区,第一个问题是,设置Survivor区的意义在哪 ...
- BLOB 和 TEXT 有什么区别?
BLOB 是一个二进制对象,可以容纳可变数量的数据.TEXT 是一个不区分大小写 的 BLOB. BLOB 和 TEXT 类型之间的唯一区别在于对 BLOB 值进行排序和比较时区分大小 写,对 TEX ...
- MySQL_fetch_array 和 MySQL_fetch_object 的区别是 什么?
以下是 MySQL_fetch_array 和 MySQL_fetch_object 的区别: MySQL_fetch_array() – 将结果行作为关联数组或来自数据库的常规数组返回. MySQL ...
- 详细描述一下 Elasticsearch 索引文档的过程 ?
这里的索引文档应该理解为文档写入 ES,创建索引的过程. 文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程. 记住官方文档中的这个图. 第一步:客户写集群某节点写入数据, ...
- memcached 是如何做身份验证的?
没有身份认证机制!memcached 是运行在应用下层的软件(身份验证应该是应用 上层的职责).memcached 的客户端和服务器端之所以是轻量级的,部分原因就 是完全没有实现身份验证机制.这样,m ...
- jdk_8接口的内部内容
目标: 如何创建已定义好的接口类型的对象呢? 步骤: 实现的概述 抽象方法的使用 默认方法的使用 静态方法的使用 接口的常量使用 讲解: 实现的概述 类与接口的关系为实现关系,即类实现接口,该类可以称 ...
- Python中module文件夹里__init__.py的功能
怎么引用模块 环境:win7 + python3.5.2文档结构: -project -data -src -filterCorpus.py -translateMonolingual.py 问题 ...