libcxx 的 shared_ptr 实现分析

##以 libcxx 为对象的 shared_ptr 实现分析。##

注意:gcc 用的是 libstdc++ , 这里的 libcxxllvm 的项目. 两者的 shared_ptr 实现相似但不相同

一个 shared_ptr/weak_ptr 存放两个指针,一个是被 manage 的 Tp* ptr, 一个是管理块, cntrl.
所以一个 shared_ptr/weak_ptr 的 空间开销 = 2*sizeof pointer。待会再说 cntrl 的空间开销。
访问 ptr 的时间开销是 1,没有性能损失。

ptr 没什么好说,每一个 shared_ptr/weak_ptr 存一份。那么 cntrl 呢?
首先说 cntrl 干什么。
cntrl 块存

  1. shared_count
  2. weak_count
  3. allocator
  4. deleter
  5. ptr

这里的 weak_countweak_ptr 引用计数。待会会发现,shared_ptrweak_ptr 紧密耦合,互不可分。所以谈 shared_ptr 的实现就必须谈 weak_ptr 的实现。
反观 unique_ptr 则自成体系,与这两者无关。

画框架图一个先:
_
所以其实 shared_ptr 和 weak_ptr 的好多操作都是 cntrl 代理的。

cntrl 是什么:

cntrl 是一个叫 __shared_weak_count 的对象。而这个__shared_weak_count 是纯虚类,继承自 __shared_count.


__shared_weak_count__shared_count 干什么呢?

__shared_weak_countweak_count. __shared_countshared_count.

shared_count 做什么呢?存一共多少个 shared_ptr 共享 这个资源。怎么计数的呢?人头-1。也就是说,shared_count == 0 的时候,一共有一个 shared_ptr 占有这个资源。

接下来,__shared_count 还提供了对 shared_count 做更改的原子操作。__add_shared__release_shared, 以及做资源释放的 __on_zero_shared

__shared_weak_count 则继承自 __shared_count. 同样,有 __add_weak, __release_weak 以及做资源释放的 __on_zero_shared_weak.


现在 weak_countshared_count 都有了,但 *ptr, Deleter 和 Alloc 存哪里呢?前面说了 shared_weak_count 是纯虚类,接下来讲 shared_weak_count 的两个实现类。
这两个类叫 shared_ptr_pointer, 以及 shared_ptr_emplace。这两者的区别仅仅在于后者没有 deleter (那谁来释放资源呢?当然是 Alloc.deallocate 函数啊),所以以后只取前者分析就好了。
下一张图:cntrl 的类图:
_1


接下来是核心:资源的构造和析构。

weak_ptr 能 hold 资源吗?答案是不能。
所以一定是 shared_ptr 才能持有资源,也就是构造和析构资源。

资源的构造

一共 下面几种 情况:

  1. hold 一个已有资源:
    比如 shared_ptr<T> p(raw_pointer); 有2, 3, 4, 12, 13 五种情况
  2. 拷贝自另一个 shared_ptr.
    比如 shared_ptr p(another_shared_ptr); 有 8, 9, 10 三种情况
  3. 构造一个空 shared_ptr <nullptr>
    比如 shared_ptr<T> p; 有 1, 5, 6, 7 四种情况

  • hold 已有资源:
    这种情况下:cntrl 没有初始化。首先 new 一个 cntrl 出来。然后根据 deletor 和 allocator 还有 ptr, 设置 shared_weak_pointer 的各项值。

为了显示我读过代码,我们拎一段代码出来看一下吧!

template<class _Tp>
template<class _Yp>
shared_ptr<_Tp >::shared_ptr(_Yp* __p ,
                            typename enable_if<is_convertible<_Yp *, element_type*>:: value, __nat >::type)
    : __ptr_ (__p)
{
    unique_ptr<_Yp > __hold(__p);
    typedef __shared_ptr_pointer <_Yp*, default_delete<_Yp >, allocator<_Yp> > _CntrlBlk;
    __cntrl_ = new _CntrlBlk(__p, default_delete <_Yp>(), allocator<_Yp>());
    __hold.release ();
    __enable_weak_this (__p);
}

原型:shared_ptr(Yp* p)
解释一下 enable_if
后面那个 enable_if 是做条件检查的,基于 SFINAE。如果 Yp* 能 convert 成 element_type*, 则这个函数有定义(否则没有定义),且签名是 shared_ptr::shared_ptr(Yp* p, __nat)
__nat 是 not a type 的缩写。显然函数在声明的时候是给它赋了默认值的,这样函数看起来像 shared_ptr(Yp*) 一样了。
这么做是怕函数体里面出现类型转换错误导致编译不通过。

解释一下 unique_ptr 作甚:
这边有一个 new, new 跪了 p 是要侧漏的。先用 unique_ptr 存住就不怕 ctor 中途异常 p 没人释放了。这里插播一下,unique_ptr 除此之外还有 exception safe 的功能。常用来和 placement new 配合无痛~~人流(误~~申请空间。在 头文件 functional 里可以找到一些实例。

enable_weak_this 基本有两份重载,shared_ptr_pointer 版本没记错的话是不干活的(空函数)。不想深究。

这里注意一下 weak_countshared_count. 它们的值都是 0. 对于 shared_ptr 这是对的(use_count = shared_count+1),因为现在有一个 shared_ptr hold 资源。那对于 weak_ptr 呢?现在明明没有 weak_ptr 指向 cntrl 啊?
其实 shared_ptr 组合本身占用一个 weak_count. 等 shared_ptr 死光了,就会减一个 weak_count. 所以这里的 weak_count 也还是 0,而不是 -1. 这个待会还会提到。

  • 拷贝自另一个 shared_ptr:
    很简单啊!引用计数+1 就好了

  • 构造一个空 shared_ptr:
    很简单啊!cntrl 赋成 0(NULL) 就好了


构造完了是析构:

析构 shared_ptr 有四种情况:
1. shared_ptr 死完了。weak_ptr 也死完了
2. shared_ptr 死完了。weak_ptr 没死完
3. shared_ptr 本身就是 empty.
4. shared_ptr 没死完,weak_ptr 也没死完


  • shared_ptr 死完了。weak_ptr 也死完了

这种情况的特征是:shared_count = -1. weak_count = 0

首先,release_shared 被调用

void
__shared_weak_count::__release_shared () _NOEXCEPT
{
    if (__shared_count::__release_shared())
        __release_weak ();
}

看到调用了 __shared_count::__release_shared()

bool
__shared_count::__release_shared () _NOEXCEPT
{
    if (decrement(__shared_owners_) == - 1)
    {
        __on_zero_shared ();
        return true ;
    }
    return false;
}

decrement 是原子减。
如果没人持有这个资源了: __on_zero_shared()

template < class _Tp, class _Dp, class _Alloc>
void
__shared_ptr_pointer<_Tp , _Dp, _Alloc>::__on_zero_shared () _NOEXCEPT
{
    __data_.first ().second()( __data_.first ().first());
    __data_.first ().second().~ _Dp();
}

这个 data, 就是 cntrl 里的 <<Tp, Dp>, Alloc> compress_paircompress_pairEBO(empty base-class optimize) 的空类做压缩。
first().second() 就是 deleter, first().first() 就是 ptr. 所以第一行用人话说就是 deleter(ptr). 析构资源。
第二行用人话说就是 ~deleter(); deleter 自杀

然后回去 shared_weak_count::release_sharedrelease_weak(),
这个函数只在 shared_ptr 死光的情况下执行,执行的首要结果就是 weak_count - 1. 也就是没有 shared_ptr 指向这个资源的时候 weak_count 会 – 1.

void
__shared_weak_count ::__release_weak () _NOEXCEPT
{
    if ( decrement (__shared_weak_owners_ ) == - 1 )
        __on_zero_shared_weak ();
}

然后判断 weak_shared 死光了没。死光了,那就 on_zero_shared_weak (没死光?那就把 cntrl 留着,ptr 挂了。这里可以看到,ptr 的释放时间是 shared_ptr 死光光的时候。而 cntrl 的释放时间是 weak_ptrshared_ptr 都死光光的时候)

template < class _Tp, class _Dp , class _Alloc >
void
__shared_ptr_pointer <_Tp , _Dp, _Alloc >::__on_zero_shared_weak () _NOEXCEPT
{
    typename _Alloc :: template rebind < __shared_ptr_pointer >::other __a( __data_ .second ());
    __data_ .second ().~ _Alloc();
    __a .deallocate ( this , 1);
}

这里首先把 alloc rebind 到自己身上。然后把原先的 alloc 析构。然后把自己 delete 掉。这样就把 cntrl 干掉了。

  • shared_ptr 死完了。weak_ptr 没死完

    见上

  • shared_ptr 本身就是 empty.

    if (cntrl) 才干活

  • shared_ptr 没死完,weak_ptr 也没死完

    release_shared();

然后是 weak_ptrshared_ptr 的转换,要加锁,其实现是:

__shared_weak_count *
__shared_weak_count ::lock () _NOEXCEPT
{
    long object_owners = __shared_owners_ ;
    while ( object_owners != -1 )
    {
        if ( __sync_bool_compare_and_swap (&__shared_owners_ ,
                                         object_owners ,
                                         object_owners + 1))
            return this ;
        object_owners = __shared_owners_ ;
    }
    return 0 ;
}

主要是防止提升期间 ptr 已经跪了。

接下来分析时间和空间代价。(这个我不擅长啊 T T

  • 新建 shared_ptr 和 cntrl.

    shared_ptr 占 2*sizeof(pointer)
    cntrl 有两个 count, sizeof(long)*2. alloc, deleter 和 ptr 被压缩了,理想情况(dp ap 都是空)应该是 sizeof(pointer)+1 (?不确定,设为 DA 吧),其实一般都不会空(Deleter 就一般是 callable object)。一个虚表,sizeof(pointer). 总共,至少,有 sizeof(pointer)*3+sizeof(long)*2,外加 dp 和 alloc 占用的空间。在 64 位系统上应该是 3*8+2*8+DA = 40+DA (byte)

    时间上,

    1. unique_ptr(p);
    2. new
    3. unique_ptr.release();

三步操作。


  • 读取 ptr 与普通指针相比没有额外消耗。

  • 新建 shared_ptr (加一个 shared_count

两步操作:
1. 判断 cntrl 是否是 null.
2. cntrl->add_shared(); 这个不是虚函数,直接可搞。其最终调用是 __sync_add_and_fetch(&t, 1);据说是 lock_free 的?不造啊不好说啊
https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html
https://news.ycombinator.com/item?id=1687075


  • 析构一个 shared_ptr, shared_ptr 没死完
  1. 两个函数调用 (release_shared)
  2. 两个判断
  3. 一次原子自减

  • 析构一个 shared_ptr, shared_ptr 死完了,weak_ptr 也死完了

    先杀 ptr, 再杀 cntrl. 每次都要判断. 还有两个原子减操作。不想细想了。。。


另及:原子增减操作都涉及到 memory barrier。在访问频繁的情况下容易造成性能问题

又及:本文不要脸地抄自我自己

Leave a Reply

Your email address will not be published. Required fields are marked *