2015-09-18

memory_resourceについて

N4529: C++ Extensions for Library Fundamentals, Version 2, Working Draftでは、Library Fundamentalsという標準ライブラリに対する拡張が規定されている。これは、いずれC++規格に取り入れられる予定である。

さて、この拡張提案の中で、メモリ確保に新しいライブラリが加わった。memory_resourceだ。

メモリーリソースとは、アロケーターと同じように、メモリーの確保と解放をするためのインターフェースだ。アロケーターがテンプレートで渡されることを前提とした満たすべき要件となっているのに対し、メモリーリソースは、abstractクラスとなっている。

class memory_resource {

public:
  virtual ~memory_resource();

  void* allocate(size_t bytes, size_t alignment = max_align);
  void deallocate(void* p, size_t bytes,
                  size_t alignment = max_align);

  bool is_equal(const memory_resource& other) const noexcept;

protected:
  virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
  virtual void do_deallocate(void* p, size_t bytes,
                             size_t alignment) = 0;

  virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
};

memory_resourceは、publicなメンバーであるallocate, deallocate, is_equalを使う。これらは、同名のdo_foobarを呼び出すようになっている。memory_resourceはabstractクラスなので、実際のストレージ確保は、memory_resourceを基本クラスとしたクラスで実装する。

allocateでストレージを確保し、deallocateでストレージを解放する。is_equalは、thisでallocateしたポインターがotherのdeallocateに渡すことと、その逆に、otherでallocateしたポインターがthisのdeallocateに渡すこととが、両方ともできる場合に、trueを返す。

例えば、::operator newとdeleteを使う単純なメモリーリソースは以下のように実装できる

#include <experimental/memory_resource>

class new_delete_resource : public std::experimental::pmr::memory_resource
{
protected :
    void * do_allocate( std::size_t bytes, size_t alignment )
    {
        return ::operator new( bytes ) ;
    }

    void do_deallocate( void* p, std::size_t bytes, std::size_t alignment )
    {
        ::operator delete( p ) ;
    }

    // 常にtrue
    bool do_is_equal( memory_resource const &amp; )
    {
        return true ; 
    }
} ;

このようにして作ったメモリーリソースは、メモリーリソースのインターフェースをアロケーターのインターフェースにあわせるラッパー、polymorhipc_allocator<T>に渡すことで、アロケーターとして使えるようになる。

#include <experimental/memory_resource>

template < typename T >
using pa = std::experimental::pmr::polymorphic_allocator<T> ;

template < typename T >
using vec = std::vector< T, std::experimental::pmr::polymorphic_allocator<T> > ;

int main()
{
    auto mr = std::make_unique<new_delete_resource>() ;

    pa<int> a( mr.get() ) ;
    vec<int> v( a ) ;
}

このnew_delete_resourceのようなメモリーリソースへのポインターを返すnew_delete_resource関数がLibrary Fundamentalsに入っている。また、get/set_default_resourceという関数もある。

Library Fundamentalsでは、2つのメモリーリソースを標準で提供している

プールリソース

プールリソースとはメモリー確保を、キリのいい種類のブロックサイズごとに区切って(例えば8, 16, 32, 64バイト...など)、ブロックサイズごとにプールと呼ばれるメモリーの「チャンク」を管理して、そのプールからメモリーを割り当てる戦略を取るメモリーリソースだ。

synchronized_pool_resourceとunsynchronized_pool_resourceとがある。違いは、複数のスレッドから同期処理を行わずにアクセスできるかどうかだ。

int main()
{
    std::experimental::pmr::synchronized_pool_resource pool ;
    std::experimental::pmr::polymorphic_allocator<int> poly(pool) ;

    vec<int> v( poly ) ;
}

プールリソースは、ブロックサイズの上限を超えるサイズのストレージを確保する際は、あらかじめ上流メモリーリソース(Upstream memory resource)を使う。上流メモリーリソースは、デフォルトではデフォルトリソース(get_default_resource)が使われる。自前のメモリーリソースを使わせたい場合は、コンストラクターで指定できる。


std::experimental::pmr::synchronized_pool_resource
    pool( custom_resource{} ) ;

また、pool_options型の引数をコンストラクターに渡すと、ブロックサイズの上限を指定できる。pool_options型は以下のように定義されている。

struct pool_options {
  size_t max_blocks_per_chunk = 0;
  size_t largest_required_pool_block = 0;
};

max_blocks_per_chunkは、上流メモリーリソースから一度に確保するブロックの数を指定する。0の場合や、実装の上限を上回る場合は、実装の上限が使われる。

largest_required_pool_blockは、プールする最大のブロックサイズを指定する。0の場合や、実装の上限を上回る場合は、実装の上限が使われる。

モノトニックバッファーリソース

monotonic_buffer_resourceは、限定的な条件で、とても高速なメモリ確保と解放を行うメモリーリソースだ。

monotonic_buffer_resourceのallocateは上流メモリーリソースから十分な大きさのストレージを確保して、必要なサイズに切り分けて返す。deallocateは、何もしない。つまり、確保されたストレージは、monotonic_buffer_resourceのオブジェクトが生存する間、開放されることがない。そのため、メモリ使用量はmonotonicに増え続ける。

使用済みオブジェクトをすべて一括して解放したい場合にきわめて効率的に動く。

monotonic_buffer_resourceは、単一のスレッドからアクセスすることを想定している。allocateとdeallocateは同期しない。

コンストラクターは、上流メモリーリソースと、確保するストレージの初期サイズを指定することができる。

std::experimental::pmr::monotonic_buffer_resource( 10 * 1014 * 1024, get_default_resource() ) ;

メンバー関数にはreleaseがあり、呼び出すと今まで確保したストレージをすべて解放する。デストラクターはreleaseを呼び出す。

polymorphic_allocatorは、実行時ポリモーフィズムにより、アロケーターの挙動を実行時に変えるメモリーリソースのインターフェースを、既存のアロケーターのインターフェースにすりあわせるためのラッパーである。つまり、メモリーリソースは実行時に変えることができる。

int main()
{
    std::string input
    std::cin >> input ;

    std::experimental::pmr::memory_resource * ptr = get_default_resource ;

    std::unique_ptr< memory_resource > raii ;

    if ( input == "pool" )
    {
        raii.reset( new std::experimental::pmr::synchronized_pool_resource() ) ;
        ptr = raii.get() ;
    }
    else if( input == "monotonic" )
    {
        raii.reset( new std::experimental::pmr::monotonic_resource() ) ;
        ptr = raii.get() ;
    }

    std::experimental::pmr::polymorphic_allocator<int> poly( ptr ) ;

    std::vec< int > v( poly ) ;

}

アロケーターを実行時に決める機能はなかなか便利だ。既存のアロケーターにも欲しい。そのため、Library Fundamentalsには、既存のアロケーターのインターフェースをメモリーリソースのインターフェースにすりあわせるラッパーも用意されている。

std::experimental::pmr::resource_adaptor< std::allocator<char> > adaptor ;
std::experimental::pmr::memory_resource * ptr = &adaptor ;

ただし、アロケーターはすべての特殊化がcharにrebindできる制約を満たさなければならない。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。9月25日に筆者の書いた「C++11/14コア言語」がアスキードワンゴから出版される。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

2 comments:

Anonymous said...

GCなんかを実装するときに役に立ちそうですね。
最近メモリもだいぶん増えてきて粗い使い方してもどうにかなってしまいますがまぁ、GCにはGCの夢があるということでちょっと期待しておきます。

みちる said...

こちらを参考に使ってみたんですが、boostでもstd::experimentalでも
コンテナのコピー代入時にはpolymorphic_allocatorをコピーしてもらえないんですね
(指しているポインタがrhsのものではなくget_default_resource()になってしまう
これはさすがに罠だと思うのですが・・デフォルト初期化はnullptrの方がマシじゃないかなぁ、と個人的に思いました