关于std::shared_ptr的思考与探究

 
 
  智能指针(管理指针的模板类)是 C++11 之后添加的新特性,它们非常有用,但是智能指针经常会带来内存碎片。游戏程序的内存往往非常紧张,所以这里探究了一下 std::shared_ptr 的内存分配以及内存同步的问题。
  测试使用的是 vc141 x86 平台。

std::shared_ptr 内存分配

  std::shared_ptr 的原理是引用计数。相比裸指针,智能指针需要在内部创建用于维护和管理计数器的类。因此,当创建智能指针对象时(new或者make_shared)时,需要额外数十个字节的内存分配;此外,每次复制指针(operator=)时,计数器都需要增加或者减少。
  另外,std::unique_ptr 几乎没有缺点,它与原始指针大小相同,除了编译器模板实例化,没有额外开销。
  以下是测试代码:


//自定义内存管理 void* MyAllocate(size_t _Sz); void MyDeallocate(void* _Ptr, size_t _Sz); //自定义内存分配器 template<class _Ty> class MyAllocator; struct SharedObject //32byte { int Data[8]; }; {// 默认 std::shared_ptr<SharedObject> ptr(new SharedObject); // new 32byte new SharedObject // new 16byte new _Ref_count<T>() 计数器类 // total 48byte 内存分配两次 } {// Dealloc auto deleter = [](SharedObject* o) { o->~SharedObject(); MyDeallocate(o, sizeof(SharedObject)); }; std::shared_ptr<SharedObject>ptr( ::new(MyAllocate(sizeof(SharedObject))) SharedObject, deleter); // MyAllocate 32byte ::new( MyAllocate(T) ) SharedObject; // new 16byte new _Ref_count_del<SharedObject,deleter>() // total 48byte 指定删除器,大小仍然为 48byte,内存分配两次 } {//指定 allocator auto deleter = [](SharedObject* o) { o->~SharedObject(); MyDeallocate(o, sizeof(SharedObject)); }; std::shared_ptr<SharedObject> ptr( ::new(MyAllocate(sizeof(SharedObject))) SharedObject, deleter, MyAllocator<SharedObject>()); // MyAllocate 32byte ::new( MyAllocate(T) ) SharedObject; // MyAllocate 16byte ::new(MyAllocator<_Ref_count_del_alloc<T> >().allocate(1)) _Ref_count_del_alloc<T>(T); // total 48byte 指定分配器和删除器,大小仍然为 48byte,内存分配两次 } //使用 make_shared shared_ptr(make_shared和allocate_shared) {//make_shared std::shared_ptr<SharedObject> ptr = std::make_shared<SharedObject>(); // new 44byte new _Ref_count_obj<SharedObject> // total 44byte 使用 make_shared 节省4个字节,内存分配一次 std::weak_ptr<SharedObject> wptr = ptr; ptr.reset(); //此时,ptr 释放,将执行~SharedObject(),但不会释放内存 //只有 wptr删除后,内存才释放 } {//allocate_shared std::shared_ptr<SharedObject> ptr = std::allocate_shared<SharedObject>(MyAllocator<SharedObject>()); // MyAllocate 44byte ::new(MyAllocator<T>().allocate(1)) _Ref_count_obj_alloc<T>; // total 44byte 内存分配一次 std::weak_ptr<SharedObject> wptr = ptr; ptr.reset(); //此时,ptr 释放,将执行~SharedObject(),但不会释放内存 //只有 wptr删除后,内存才释放 }

  所以,在上述代码中,make_shared 以及 allocate_shared 只分配 44 字节,一次分配完成;new 初始化 shared_ptr 会分配 48 字节,需要两次分配。
  之所以会多出 4 字节,是因为 new 初始化 shared_ptr 采用的下图的方式,将计数器分配在其他地方,shared_ptr 会多一个 4 字节指针。

new
  而使用 make_shared 和 allocate_shared 会将计数器和类实体放在一起。
make_shared
  make_shared 是非常有用,也推荐使用的。但是实际上还有一个小小的隐患。由于计数器和类实例分配在一起,最后一个 shared_ptr 析构之后,如果存在弱引用,就会导致虽然对象已经被释放,分配的内存却没有被收回,直到最后一个 weak_ptr 被回收。所以在游戏的资源管理器中,必须定期清理无用的弱引用,才能真正达到释放内存的效果。

std::shared_ptr 线程安全性

  简单回顾一下 std::shared_ptr 的成员变量与成员函数。就会发现 shared_ptr 的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员(指向被管理对象的指针,和指向控制块的指针),读写操作不能原子化。根据文档Boost-ThreadSafety, shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:
 
• 一个 shared_ptr 对象实体可被多个线程同时读取
 
• 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
 
• 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁


Tags :

About the Author

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注