智能指针(管理指针的模板类)是 C++11 之后添加的新特性,它们非常有用,但是智能指针经常会带来内存碎片。游戏程序的内存往往非常紧张,所以这里探究了一下 std::shared_ptr 的内存分配以及内存同步的问题。
测试使用的是 vc141 x86 平台。
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 字节指针。
简单回顾一下 std::shared_ptr 的成员变量与成员函数。就会发现 shared_ptr 的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员(指向被管理对象的指针,和指向控制块的指针),读写操作不能原子化。根据文档Boost-ThreadSafety, shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:
• 一个 shared_ptr 对象实体可被多个线程同时读取
• 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
• 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁