allocator 篇
allocator,一般情况下我们都不会见到或用到。不过大概也清楚,container 们基本都有一个模板参数,比如说 vector 的。
template<
class T,
class Allocator = std::allocator<T>
> class vector;
第二个模板参数就是 allocator, 默认是 std::allocator。allocator 是用来分配内存的,也就是在你觉得默认的分配内存方式效率等方面存在问题的时候,你可以自己 customize 一个更好的 allocator。
关于 allocator 更多内的介绍可以 wiki 。
allocator 也有很蛋疼的地方。allocator 作为模板参数,导致两个用不同 allocator 的 vector 不是一个类型,无法赋值(类型不同怎么在一起)。另外老标准是要求 allocator 是无状态的,这很难用。
C++11 之后,allocator 可以有状态,而所有操作需要通过 std::allocator_traits. 来进行(真是蛋疼)。
而让人更疼的是,c++11 还引入了 [allocator_arg] (http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2554.pdf) 。原因呢,当初搞 std::tuple, std::function, std::pair 时候上面没加 allocator 的模板参数,于是现在想让他们在 allocator 里面分配怎么办?增加带 allocator_arg 参数构造函数,这个构造函数有 Alloc 模板参数,提供 allocator。(蛋疼。不过目前 allocator_arg 还没有被引入 std::pair )
std::scoped_allocator_adaptor。这货暂时不想吊他。
附上一些 allocator 的参考资料 * n1850 * improving perfomance with custom pool allocator * what are allocators good for?
暂时不想深究 allocator,只是想解决一下之前 shared_ptr 里面 inplace 分配的问题。所以不做过多的分析,只搞明白 allocator 和 trait 是怎么用的好了。
来到 include/bits/allocator.h
总是说标准库里面会做一些蛋疼的事情,比如说 allocator 也有对 void 的特化。
template<>
class allocator<void>
template<typename _Tp>
class allocator: public __allocator_base<_Tp>
{
public:
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef _Tp* pointer;
typedef const _Tp* const_pointer;
typedef _Tp& reference;
typedef const _Tp& const_reference;
typedef _Tp value_type;
template<typename _Tp1>
struct rebind
{ typedef allocator<_Tp1> other; };
#if __cplusplus >= 201103L
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 2103. std::allocator propagate_on_container_move_assignment
typedef true_type propagate_on_container_move_assignment;
#endif
allocator() throw() { }
allocator(const allocator& __a) throw()
: __allocator_base<_Tp>(__a) { }
template<typename _Tp1>
allocator(const allocator<_Tp1>&) throw() { }
~allocator() throw() { }
// Inherit everything else.
};
恩,又是一层包装,暂时找不到封装的理由。
template<typename _T1, typename _T2>
inline bool
operator==(const allocator<_T1>&, const allocator<_T2>&)
_GLIBCXX_USE_NOEXCEPT
{ return true; }
template<typename _Tp>
inline bool
operator==(const allocator<_Tp>&, const allocator<_Tp>&)
_GLIBCXX_USE_NOEXCEPT
{ return true; }
template<typename _T1, typename _T2>
inline bool
operator!=(const allocator<_T1>&, const allocator<_T2>&)
_GLIBCXX_USE_NOEXCEPT
{ return false; }
template<typename _Tp>
inline bool
operator!=(const allocator<_Tp>&, const allocator<_Tp>&)
_GLIBCXX_USE_NOEXCEPT
{ return false; }
为了简化 allocator 和提升效率,比较 operator 是这样做的。
注意到
#if _GLIBCXX_EXTERN_TEMPLATE
extern template class allocator<char>;
extern template class allocator<wchar_t>;
#endif
而在 src/c++98/allocator-inst.cc 中进行显示实例化
namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
template class allocator<char>;
template class allocator<wchar_t>;
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace
这是为 std::string 做的。这样显式实例化可以让你使用 std::string 时候,不用再生成 allocator 模板的代码,而直接链接到 lib 里面的代码。
外面还有点代码无关紧要,先进 __allocator_base 里面来看。 __allocator_base 应该在 #include <bits/c++allocator.h> 里面,然而目前并没有这个文件。。。makefile 搞得鬼
Makefile.am
$(LN_S) ${glibcxx_srcdir}/$(ALLOCATOR_H) c++allocator.h || true ;\
configure
# Set configure bits for specified locale package
case ${enable_libstdcxx_allocator_flag} in
bitmap)
ALLOCATOR_H=config/allocator/bitmap_allocator_base.h
ALLOCATOR_NAME=__gnu_cxx::bitmap_allocator
;;
malloc)
ALLOCATOR_H=config/allocator/malloc_allocator_base.h
ALLOCATOR_NAME=__gnu_cxx::malloc_allocator
;;
mt)
ALLOCATOR_H=config/allocator/mt_allocator_base.h
ALLOCATOR_NAME=__gnu_cxx::__mt_alloc
;;
new)
ALLOCATOR_H=config/allocator/new_allocator_base.h
ALLOCATOR_NAME=__gnu_cxx::new_allocator
;;
pool)
ALLOCATOR_H=config/allocator/pool_allocator_base.h
ALLOCATOR_NAME=__gnu_cxx::__pool_alloc
;;
esac
看来是对 new, pool, mt, bitmap 这几种做了封装。我想我们的 default 应该是 new allocator 吧~
if test $enable_libstdcxx_allocator_flag = auto; then
case ${target_os} in
linux* | gnu* | kfreebsd*-gnu | knetbsd*-gnu)
enable_libstdcxx_allocator_flag=new
;;
*)
enable_libstdcxx_allocator_flag=new
;;
esac
fi
有空可以研究一下 bitmap 和 pool,mt 这几个 allocator 是干嘛的。先看 new 的吧。
template<typename _Tp>
using __allocator_base = __gnu_cxx::new_allocator<_Tp>;
来到 ext/new_allocator.h,废话不看,直接往下拉
pointer
address(reference __x) const _GLIBCXX_NOEXCEPT
{ return std::__addressof(__x); }
const_pointer
address(const_reference __x) const _GLIBCXX_NOEXCEPT
{ return std::__addressof(__x); }
// NB: __n is permitted to be 0. The C++ standard says nothing
// about what the return value is when __n == 0.
pointer
allocate(size_type __n, const void* = 0)
{
if (__n > this->max_size())
std::__throw_bad_alloc();
return static_cast<_Tp*>(::operator new(__n * sizeof(_Tp)));
}
// __p is not permitted to be a null pointer.
void
deallocate(pointer __p, size_type)
{ ::operator delete(__p); }
size_type
max_size() const _GLIBCXX_USE_NOEXCEPT
{ return size_t(-1) / sizeof(_Tp); }
#if __cplusplus >= 201103L
template<typename _Up, typename... _Args>
void
construct(_Up* __p, _Args&&... __args)
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
template<typename _Up>
void
destroy(_Up* __p) { __p->~_Up(); }
#else
allocator 的内容已经尽收眼底。address 是拿地址,allocate 是分配 n 个对象的内存,deallocate 释放内存。 construct 则是 placement new 掉构造,destroy 则是调析构。
allocate 大概的功能已经清楚了,我们来看 allocator_trait。
include/bits/allocate_trait 其实也没什么内容,就是一层代理
static pointer
allocate(_Alloc& __a, size_type __n)
{ return __a.allocate(__n); }
static pointer
allocate(_Alloc& __a, size_type __n, const_void_pointer __hint)
{ return _S_allocate(__a, __n, __hint); }
static void deallocate(_Alloc& __a, pointer __p, size_type __n)
{ __a.deallocate(__p, __n); }
template<typename _Tp, typename... _Args>
static auto construct(_Alloc& __a, _Tp* __p, _Args&&... __args)
-> decltype(_S_construct(__a, __p, std::forward<_Args>(__args)...))
{ _S_construct(__a, __p, std::forward<_Args>(__args)...); }
template <class _Tp>
static void destroy(_Alloc& __a, _Tp* __p)
{ _S_destroy(__a, __p); }
_S_allocate 可以带 hint,当然这不是我们现在关心的重点。 看下 _S_construct
template<typename _Tp, typename... _Args>
static _Require<__has_construct<_Tp, _Args...>>
_S_construct(_Alloc& __a, _Tp* __p, _Args&&... __args)
{ __a.construct(__p, std::forward<_Args>(__args)...); }
template<typename _Tp, typename... _Args>
static
_Require<__and_<__not_<__has_construct<_Tp, _Args...>>,
is_constructible<_Tp, _Args...>>>
_S_construct(_Alloc&, _Tp* __p, _Args&&... __args)
{ ::new((void*)__p) _Tp(std::forward<_Args>(__args)...); }
原来是重载。不过 __has_construct 和 is_constructible 有什么区别呢?
template<typename _Tp, typename... _Args>
struct __construct_helper
{
template<typename _Alloc2,
typename = decltype(std::declval<_Alloc2*>()->construct(
std::declval<_Tp*>(), std::declval<_Args>()...))>
static true_type __test(int);
template<typename>
static false_type __test(...);
using type = decltype(__test<_Alloc>(0));
};
template<typename _Tp, typename... _Args>
using __has_construct
= typename __construct_helper<_Tp, _Args...>::type;
原来 __has_construct 是判断 allocator 上是否能 construct 这个东东。不过按理说 allocator 的 construct 也是掉 placement new,两者会有不同么?
好吧,关于这点要回到 allocator_trait 的意义。The allocator_traits class template provides the standardized way to access various properties of allocators。
于是当 Alloc 不存在 construct 函数的时候。。。allocate_trait 就主动帮忙。destroy 也是同理。
还有一个东西,rebind,昨天就卡在这里。
template<typename _Tp>
using rebind_alloc = typename __alloctr_rebind<_Alloc, _Tp>::__type;
template<typename _Tp>
using rebind_traits = allocator_traits<rebind_alloc<_Tp>>;
来看 __alloctr_rebind
template<typename _Alloc, typename _Tp,
bool = __alloctr_rebind_helper<_Alloc, _Tp>::__type::value>
struct __alloctr_rebind;
template<typename _Alloc, typename _Tp>
struct __alloctr_rebind<_Alloc, _Tp, true>
{
typedef typename _Alloc::template rebind<_Tp>::other __type;
};
template<template<typename, typename...> class _Alloc, typename _Tp,
typename _Up, typename... _Args>
struct __alloctr_rebind<_Alloc<_Up, _Args...>, _Tp, false>
{
typedef _Alloc<_Tp, _Args...> __type;
};
看一下 __alloctr_rebind_helper<_Alloc, _Tp>::__type::value 什么时候是 true 什么时候是 false
template<typename _Alloc, typename _Tp>
class __alloctr_rebind_helper
{
template<typename _Alloc2, typename _Tp2>
static constexpr true_type
_S_chk(typename _Alloc2::template rebind<_Tp2>::other*);
template<typename, typename>
static constexpr false_type
_S_chk(...);
public:
using __type = decltype(_S_chk<_Alloc, _Tp>(nullptr));
};
也就是看 _Alloc2::template rebind<_Tp2>::other* 是否存在。 之前没有注意看 allocator 里面的 rebind 成员,回头喵一眼。
template<typename _Tp1>
struct rebind
{ typedef new_allocator<_Tp1> other; };
好吧,也就是 rebind 了另一个类型的 allocator。反正 new allocator 没状态,随便给你无所谓。
等下, _Alloc2::template 这种东西第一次见到,为什么要这么写?(我很土的)这是因为 _Alloc2 现在还是模板类型嘛,还木有 _Tp,而 rebind 并不需要类模板参数 _Tp,给一个 _Tp1 就够了。
__alloctr_rebind_helper 在判断 allocator 里面是否有 rebind 支持。如果有 rebind 的话,后面的 __alloctr_rebind 就会把这个 rebind 提供出去;如果没有呢(false),则会自己定义一个 typedef _Alloc<_Tp, _Args…> __type; ,将 _Args… bind 到 allocator 上,allocator<_Tp> 变成了 Allocator<_Tp, _Args …>。
于是跳到最前面, rebind_alloc 就是 rebind 之后的 allocator 类型啦,rebind_traits 则是这个 allocator 上加 trait 之后的结果。
那么,现在是时候接过昨天的烂尾工程了
还是 __shared_count 的构造函数。
template<typename _Tp, typename _Alloc, typename... _Args>
__shared_count(_Sp_make_shared_tag, _Tp*, const _Alloc& __a,
_Args&&... __args)
: _M_pi(0)
{
typedef _Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp> _Sp_cp_type;
typedef typename allocator_traits<_Alloc>::template
rebind_traits<_Sp_cp_type> _Alloc_traits;
typename _Alloc_traits::allocator_type __a2(__a);
_Sp_cp_type* __mem = _Alloc_traits::allocate(__a2, 1);
__try
{
_Alloc_traits::construct(__a2, __mem, std::move(__a),
std::forward<_Args>(__args)...);
_M_pi = __mem;
}
__catch(...)
{
_Alloc_traits::deallocate(__a2, __mem, 1);
__throw_exception_again;
}
}
首先定义了 _Sp_cp_type 就是我们 counter 的 type,然后 rebind_traits 将原来 _Alloc<_Tp> rebind 到了 _Sp_cp_type 变成了 _Alloc<_Sp_cp_type>,然后用原来的 _Alloc __a 构造一个新的 _Alloc<_Sp_cp_type> __a2,接下来就是 construct。发现 construct 会接受 _Alloc<_Tp>。
template<typename... _Args>
_Sp_counted_ptr_inplace(_Alloc __a, _Args&&... __args)
: _M_impl(__a)
{
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 2070. allocate_shared should use allocator_traits<A>::construct
allocator_traits<_Alloc>::construct(__a, _M_ptr(),
std::forward<_Args>(__args)...); // might throw
}
再看 Impl
class _Impl : _Sp_ebo_helper<0, _Alloc>
{
typedef _Sp_ebo_helper<0, _Alloc> _A_base;
public:
explicit _Impl(_Alloc __a) noexcept : _A_base(__a) { }
_Alloc& _M_alloc() noexcept { return _A_base::_S_get(*this); }
__gnu_cxx::__aligned_buffer<_Tp> _M_storage;
};
注
_Sp_ebo_helper 是做 empty base optimization 的,_M_storage 就是 _Tp 真正存在的地方。 注意到这里还用到 __aligned_buffer,rebind 之后 counter 部分应该是在 _Tp 的后面,用对齐 buffer 是为了对齐 counter 部分。
相应 _M_destroy 和 _M_dispose 里面也就是各种的 deallocate 和 destroy 了。
allocate 分配的是连续的内存,只有在 _M_destroy 的时候才会最后释放。所以说,inplace 也有一个坏处,就是只要存在 weak_ptr 时,就算 shared_ptr 都挂掉,对象已经失效,但内存还在占用着。
好了,昨天的烂尾补上了~~
总结一下
- lib 的设计真是一件非常蛋碎的事情,稍有不慎就各种败笔(allocator可算一例?)
- allocator 的初衷是好的,不过日常生活中一般用不到啦~ 然后之前实习的时候有用 mem_pool,也没借助 allocator 而是直接在 mem_pool 上 new 什么的。还是看怎么方便怎么来
- ebo 可谓在 stl 里被用烂了
- 找时间可以分析一下几种 allocator,mt(多线程)的 mem_pool 应该比较好玩。不过现在大家都用 tcmalloc 这种东西了。。。。。