std::shared_ptrが内部で確保するメモリについての調査&スレッド対応について

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);
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.