C++11で追加されたstd::shared_ptrやstd::unique_ptrなどのポインタを管理するテンプレートクラスは本当に便利です。ですが、内部で確保されるメモリや処理について考えておかないと思わぬところで処理速度の低下やメモリ不足に陥ります。特にゲームプログラムのメモリ管理は厳しくなる傾向があるため、shared_ptrが内部で確保するメモリのサイズや回数について調べてみました。ちなみにunique_ptrはほぼデメリットはありません。生ポインタと同じサイズ、includeとテンプレート処理でコンパイルがほんの少し遅くなる程度です。
※VisualStudio2017 x86プラットフォームでの調査 他の環境では結果が異なる場合があります
std::shared_ptrの内部処理
shared_ptrは参照カウンタを利用したスマートポインタなので、カウンターを保持、管理するクラスが内部で作成されます。shared_ptrを作成する毎に数十byteの追加のメモリ確保を行います。また、ポインターをコピー(operator=)する毎に参照カウンタの増減が発生します。shared_ptrでの主な追加コストはこの2つになります。
std::shared_ptr内部で確保されるメモリサイズと回数
shared_ptrの一番のコストはやはりメモリ確保になります。shared_ptrのコンストラクターの引数を変えることで、独自のアロケータを使用したりメモリ確保の回数を減らしたりできるようになっています。それぞれについて検証するプログラムを作成しました。ソースコードと検証結果は以下のようになります。
//自作のメモリ管理
void* MyAllocate(size_t _Sz);
void MyDeallocate(void* _Ptr, size_t _Sz);
//自作アロケータ(STLで使用するアロケータと同じもの)
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<略>() 参照カウンタを管理するクラス
// total 48byte
// defaultのnewで2回メモリ確保
}
{//デーリータ指定
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(略) ) SharedObject;
// new 16byte new _Ref_count_del<SharedObject,deleter>()
// total 48byte
// _Ref_count_del<略>()は参照カウンタ+デリーターを管理するクラス
// デリータを指定してもサイズは16byteのまま
// _Ref_count_del<略>クラスはdefaultのnewでメモリ確保
}
{//アロケータ指定
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(略) ) SharedObject;
// MyAllocate 16byte ::new(MyAllocator<_Ref_count_del_alloc<略> >().allocate(1)) _Ref_count_del_alloc<略>(略);
// total 48byte 自作アロケータからメモリ確保
// 参照カウンタクラスを専用アロケータで作成可能
// MyAllocator<SharedObject>()は、SharedObject本体ではなく
// _Ref_count_del_alloc<SharedObject,deleter>という参照カウンタとデリーター
// を管理するクラス(16byte)を作成するアロケータに変換される
}
//参照カウンタとまとめてメモリ確保する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
// _Ref_count_obj<SharedObject>はSharedObjectと参照カウンタを1つにまとめたクラス
// 1つにまとめているため追加メモリは12byteに効率化
// defaultのnewで1回メモリ確保
}
{//allocate_shared make_sharedのアロケータ指定
std::shared_ptr<SharedObject> ptr
= std::allocate_shared<SharedObject>(MyAllocator<SharedObject>());
// MyAllocate 44byte ::new(MyAllocator<略>().allocate(1)) _Ref_count_obj_alloc<略>;
// total 44byte
// 自作アロケータから1回メモリ確保
std::weak_ptr<SharedObject> wptr = ptr;
ptr.reset();
//この時点で~SharedObject()は実行されるが、メモリは解放されない
//wptr削除時にまとめてメモリ解放
}
std::shared_ptrメモリ操作対応表
Windows(x86) shared_ptr内部で行われるメモリ操作。メモリサイズはあくまで目安です。クラスのアラインメントや環境によって数値が変わる場合があります。
shared_ptrで管理するクラスT | カウンター管理クラス | ||||
コンストラクター | メモリ確保 | メモリ解放 | サイズ | メモリ確保/解放 | サイズ |
shared_ptr<T>(T* p) | new | delete | sizeof(T) | new/delete | 16byte |
shared_ptr<T>(T* p, Deleter) | 任意 | Deleter(p); | sizeof(T) | new/delete | 16byte |
shared_ptr<T>(T* p, Deleter, MyAllocator<T>()) | 任意 | Deleter(p); | sizeof(T) | MyAllocator | 16byte |
make_shared<T>(…); | なし※ | ~T() | 0 | new/delete | sizeof(T) +12byte |
allocate_shared<T>(MyAllocator<T>(),…) | なし※ | ~T() | 0 | MyAllocator | sizeof(T) +12byte |
※make_sharedとallocate_sharedはカウンター管理クラスとまとめて1回のメモリ確保
make_sharedとallocate_sharedの注意点
1回のメモリ確保で済むため効率的ですが、weak_ptrで弱参照している場合、参照カウンタ0になりデストラクタが実行されてもメモリは解放されません。参照カウンタとまとめてメモリ確保しているためweak_ptrからの弱参照がなくなるまで無駄なメモリが残り続けます。サイズの大きいクラスをmake_sharedやallocate_sharedで作成しweak_ptrから参照するとメモリ効率が悪化する可能性があります。
shared_ptrでのスレッド対応
shared_ptrでスレッドセーフな処理は、参照カウンタの操作のみです。カウンタの整数値をアトミック操作しているだけで、他の操作はスレッドセーフではありません。->演算子でのメンバ変数や関数アクセスがスレッドセーフになるわけではありません。自分で実装する必要があります。それと、マルチスレッドでshared_ptrを使う際はデストラクタがいつ、どこで、だれから呼ばれるか予測が難しいため、デストラクタから呼ばれる関数がスレッドセーフかどうか十分確認することをお勧めします。プログラムの構造や処理時間が変わることでshared_ptrの参照カウンタが0になる場所、タイミングが変わり、突然原因不明のバグに襲われるかもしれません。
shared_ptr用メモリアロケーター例
VisualStuio2017の<xmemory0>のallocatorクラス(STLのデフォルトアロケータ)のメモリ確保関数を置き換えたものです。::operator newをWindows専用の_aligned_mallocに変更。
追記2017/9/6
VisualStuio2017更新で_THROW0が廃止されました。_THROW0を削除するか定義しないとコンパイルエラーになります。その他のVisualStudio固有の定義(_NOEXCEPTなど)は、独自の定義に変更してください。
void *MyAllocate(size_t _Sz)
{
return _aligned_malloc(_Sz, 16);//XMVECTORなどalign 16byteに対応
}
void MyDeallocate(void * _Ptr, size_t _Sz)
{
if(!_Ptr)return;
_aligned_free(_Ptr);
}
// 自作メモリアロケータ
// VisualStudio2017 <xmemory0> alloctorを改造
// TEMPLATE CLASS allocator
template<class _Ty>
class MyAllocator
{ // generic allocator for objects of class _Ty
public:
static_assert(!std::is_const<_Ty>::value,
"The C++ Standard forbids containers of const elements "
"because allocator<const T> is ill-formed.");
typedef void _Not_user_specialized;
typedef _Ty value_type;
typedef value_type *pointer;
typedef const value_type *const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef std::true_type propagate_on_container_move_assignment;
typedef std::true_type is_always_equal;
template<class _Other>
struct rebind
{ // convert this type to allocator<_Other>
typedef MyAllocator<_Other> other;
};
pointer address(reference _Val) const _NOEXCEPT
{ // return address of mutable _Val
return (_STD addressof(_Val));
}
const_pointer address(const_reference _Val) const _NOEXCEPT
{ // return address of nonmutable _Val
return (_STD addressof(_Val));
}
MyAllocator() _THROW0()
{ // construct default allocator (do nothing)
}
MyAllocator(const MyAllocator<_Ty>&) _THROW0()
{ // construct by copying (do nothing)
}
template<class _Other>
MyAllocator(const MyAllocator<_Other>&) _THROW0()
{ // construct from a related allocator (do nothing)
}
template<class _Other>
MyAllocator<_Ty>& operator=(const MyAllocator<_Other>&)
{ // assign from a related allocator (do nothing)
return (*this);
}
void deallocate(pointer _Ptr, size_type _Count)
{ // deallocate object at _Ptr
MyDeallocate(_Ptr, sizeof(_Ty));
}
_DECLSPEC_ALLOCATOR pointer allocate(_CRT_GUARDOVERFLOW size_type _Count)
{ // allocate array of _Count elements
if(_Count == 0)return nullptr;
return (static_cast<pointer>(MyAllocate(_Count*sizeof(_Ty))));
}
_DECLSPEC_ALLOCATOR pointer allocate(_CRT_GUARDOVERFLOW size_type _Count, const void *)
{ // allocate array of _Count elements, ignore hint
return (allocate(_Count));
}
template<class _Objty,
class... _Types>
void construct(_Objty *_Ptr, _Types&&... _Args)
{ // construct _Objty(_Types...) at _Ptr
::new ((void *)_Ptr) _Objty(_STD forward<_Types>(_Args)...);
}
template<class _Uty>
void destroy(_Uty *_Ptr)
{ // destroy object at _Ptr
_Ptr->~_Uty();
}
size_t max_size() const _NOEXCEPT
{ // estimate maximum array size
return ((size_t)(-1) / sizeof(_Ty));
}
};
template<class _Ty,
class _Other> inline
bool operator==(const MyAllocator<_Ty>&,
const MyAllocator<_Other>&) _THROW0()
{ // test for allocator equality
return (true);
}
template<class _Ty,
class _Other> inline
bool operator!=(const MyAllocator<_Ty>&,
const MyAllocator<_Other>&) _THROW0()
{ // test for allocator inequality
return (false);
}