コンテンツにスキップ

最終更新:2026-10-12

C++17 PMR メモリリソース

1.導入

C++17より前では、標準ライブラリはstd::allocatorを提供していました。C++17ではこの機能が強化され、polymorphic_allocatorが導入されました。

C++17より前で、std::allocatorを使ってメモリプールをカスタマイズしたい場合、従来の仮想関数による多態の方法は使えませんでした。std::allocatorは仮想関数を提供していないからです。したがって、継承は明らかに良い選択ではありませんが、次のようにクラスのメンバとして使うことはできます:

template <typename Allocator = std::allocator<uint8_t>>
class MemoryPool {
 public:
  explicit STLMemoryPool(const Allocator& alloc) : alloc_(alloc) {}

  Status Allocate(int64_t size, uint8_t** out) {
    try {
      *out = alloc_.allocate(size);
    } catch (std::bad_alloc& e) {
      return Status::OutOfMemory(e.what());
    }
    return Status::OK();
  }

  void Free(uint8_t* buffer, int64_t size) {
    alloc_.deallocate(buffer, size);
  }

 private:
  Allocator alloc_;
};

C++17より前で、メモリの割り当て・解放時にprint処理やカスタム処理を行いたい場合、2つの方法がありました:

  • グローバルなnew/deleteをカスタマイズする
void* operator new(std::size_t size) {
  void* ptr = malloc(size);
  std::cout << "Allocated: " << size << " bytes at address " << ptr << std::endl;
  return ptr;
}

void operator delete(void* ptr, std::size_t n) noexcept {
  std::cout << "Deallocated: " << n << " bytes at address " << ptr << std::endl;
  free(ptr);
}
  • allocatorをカスタマイズする
class allocator {
 public:
  using value_type = T;
  value_type* allocate(std::size_t n) {
    value_type* p = static_cast<value_type*>(::operator new(n * sizeof(value_type)));
    std::cout << "Allocated: " << n * sizeof(value_type) << " bytes at address "
              << static_cast<void*>(p) << std::endl;
    return p;
  }

  void deallocate(value_type* p, std::size_t n) noexcept {
    std::cout << "Deallocated: " << n * sizeof(value_type) << " bytes at address "
              << static_cast<void*>(p) << std::endl;
    ::operator delete(p);
  }
};

std::vector<int, allocator<int>> vec;

では、このほかにも方法はあるのでしょうか?

C++17以降では、多態的なメモリ管理ツールであるpolymorphic memory resources(pmr)を使えます。使い方:

#include <memory_resources>

pmr::monotonic_buffer_resource res(10);
pmr::vector<vector<int>> vec1(&res);

上の問題に対しては、memory_resourceを継承することで対処できます。下のコードに関する説明は、後述のmemory_resourceの節をご覧ください。

class TrackAllocator : public std::pmr::memory_resource {
    void* do_allocate(std::size_t bytes, std::size_t alignment) override {
        void* p = std::pmr::new_delete_resource()->allocate(bytes, alignment);
        std::cout << "do_allocate: " << bytes << " bytes at " << p
                  << " alignment: " << alignment << std::endl;
        return p;
    }

    void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
      std::cout << "do_deallocate: " << bytes << " bytes at "
                << " alignment: " << alignment << std::endl;
      return std::pmr::new_delete_resource()->deallocate(p, bytes, alignment);
    }

    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return std::pmr::new_delete_resource()->is_equal(other);
    }
};

これで、上と同じ効果を実現できました。

2.memory_resource

STLにおけるstd::par::memory_resourceクラスの実装は比較的シンプルで、以下は疑似コードによる実装です:

class memory_resource {
  static const size_t __max_align = alignof(max_align_t);

public:
  virtual ~memory_resource();

  void* allocate(size_t __bytes, size_t __align = __max_align) {
    return do_allocate(__bytes, __align);
  }

  void deallocate(void* __p, size_t __bytes, size_t __align = __max_align) {
    do_deallocate(__p, __bytes, __align);
  }

  bool is_equal(const memory_resource& __other) const noexcept { 
    return do_is_equal(__other); 
  }

private:
  virtual void* do_allocate(size_t, size_t)                       = 0;
  virtual void do_deallocate(void*, size_t, size_t)               = 0;
  virtual bool do_is_equal(memory_resource const&) const noexcept = 0;
};

デフォルトのstd::allocatorとは異なり、memory_resourceは多態の能力を提供し、ユーザーは継承によって次の3つのインターフェースをオーバーライドできます:

  • do_allocate
  • 指定したサイズとアライメント要件のメモリを割り当てるために使います。
  • do_deallocate
  • 以前に do_allocate で割り当てたメモリを解放します。
  • do_is_equal
  • 現在のメモリリソースオブジェクトが、別のメモリリソースオブジェクトと等しいかどうかを確認します。

そこで、先ほどのTrackAllocatorは、上の3つの関数インターフェースをオーバーライドするだけで済みます。

注意深い方はお気づきでしょうが、これらのインターフェースに加えて、memory_resourceは外部向けの3つのインターフェースも提供しています。さらに、グローバルなインターフェースも提供されています:

memory_resource* new_delete_resource() noexcept;
memory_resource* null_memory_resource() noexcept;
memory_resource* get_default_resource() noexcept;
memory_resource* set_default_resource(memory_resource* __r) noexcept;
  • new_delete_resource

memory_resourceポインタを返します。このポインタはnew演算子とdelete演算子を使ってメモリの割り当てと解放を行います。このリソースはグローバルのnew・delete演算子で実装されているため、デフォルトのメモリリソースマネージャです。このmemory_resourceのサブクラスは__resource_adaptor_impであり、上の3つのインターフェースのオーバーライドを担います。

  • null_memory_resource

何も実行しない空のメモリリソースを表すmemory_resourceポインタを返します。実際のメモリ割り当てを行わずにテストやプレースホルダをしたいときに、このリソースを使えます。STLのソース実装では次のように定義されています

inline memory_resource*
null_memory_resource() noexcept {
    class type final : public memory_resource {};
}

typeはサブクラスとして上の3つのインターフェースをオーバーライドしており、do_allocateインターフェースは割り当てエラーの例外を投げます。意味論として割り当てを許さないからです。

do_allocate(size_t, size_t) override
    { std::__throw_bad_alloc(); }

したがって、null_memory_resourceを使うときはtry catchが必要です。例えば、stringの文字数が16未満の場合はスタック上に割り当てられ、このときはここのmemory_resourceは使われないため正常に動作しますが、長さが16以上になると、std::bad_alloc例外が投げられます。

std::pmr::memory_resource* default_resource = std::pmr::null_memory_resource();

std::pmr::set_default_resource(default_resource);

try {
  std::pmr::vector<int> data{1};
} catch (std::bad_alloc e) {
  std::cerr << "bad_alloc is thrown vector" << std::endl;
}

std::pmr::string str1{"hello world hel"};
std::cerr << str1 << " length:" << str1.size() << std::endl;

try {
  std::pmr::string str1{"hello world hel0"};
} catch (std::bad_alloc e) {
  std::cerr << "bad_alloc is thrown string" << std::endl;
}
  • get_default_resource

現在のデフォルトのグローバルメモリリソースマネージャを返します。デフォルトではnew_delete_resource()が返すのと同じリソースですが、set_default_resource関数でデフォルトのリソースを変更できます。

  • set_default_resource

メモリリソースマネージャを設定します。引数には空を渡せます(デフォルトのリソースを返します)。そうでなければ渡されたリソースに設定し、以前のデフォルトリソースを返します。

3.メモリ再利用

std::pmr::monotonic_buffer_resourceはバッファであり、メモリの再利用という点で重要な優位性があります。monotonic_buffer_resourceはmemory_resourceを継承して上の3つのインターフェースをオーバーライドし、内部に上流のmemory_resourceを保持します。現在のメモリが不足すると、上流から取りに行きます。これによりメモリ断片化を抑え、メモリ利用率を高めることができます。

以下にstd::pmr::monotonicの使い方をいくつか挙げます。

使い方1 - 固定バッファを使って monotonic_buffer_resource を作成:

char buf1[30] = {};
std::fill_n(std::begin(buf1), std::size(buf1) - 1, '_');
std::pmr::monotonic_buffer_resource pool1{std::data(buf1), std::size(buf1)};
std::pmr::vector<int> myVec1{&pool1};

使い方2 - 上流リソースを利用して monotonic_buffer_resource を作成:

std::pmr::memory_resource* upstreamResource2 = std::pmr::get_default_resource();
std::pmr::monotonic_buffer_resource pool2(upstreamResource2);
{
    std::pmr::vector<double> numbers(&pool2);
    for (double d = 0.1; d < 1.0; d += 0.1) {
        numbers.push_back(d);
    }
}

使い方3 - std::pmr::monotonic_buffer_resourceを継承:

class MyMonotonicBufferResource : public std::pmr::monotonic_buffer_resource {
public:
    using std::pmr::monotonic_buffer_resource::monotonic_buffer_resource; // Inherit constructors

    // Public function to access do_allocate
    void* allocate(std::size_t bytes, std::size_t alignment) {
        return do_allocate(bytes, alignment);
    }

    // Public function to access do_deallocate
    void deallocate(void* p, std::size_t bytes, std::size_t alignment) {
        do_deallocate(p, bytes, alignment);
    }
};

MyMonotonicBufferResource pool3;
// Allocate memory
void* mem = pool3.allocate(100, alignof(int));

// Use the allocated memory (this is just an example)
std::pmr::vector<int> numbers({1, 2, 3}, &pool3);

// Deallocate memory
pool3.deallocate(mem, 100, alignof(int));

std::cout << "Usage 3 - Inherit: \n\n";

4.pool resource

synchronized_pool_resourceはスレッドセーフなメモリプールリソースクラスです。その設計目的は、マルチスレッド環境で安全にメモリの割り当てと解放を行うことです。複数のスレッドが並行してメモリの割り当てや解放を試みるとき、synchronized_pool_resource は同期機構を使ってスレッド安全性を保証します。共有されるメモリプールを保護するためにロックが必要となるため、通常は多少の性能オーバーヘッドが生じます。

これに対応するのがunsynchronized_pool_resourceで、スレッド安全性はユーザー自身が保証する必要があります。

synchronized_pool_resourceのソース実装は、実質的にmutex + unsynchronized_pool_resourceです。

class synchronized_pool_resource : public memory_resource {

public:
    // some interface
private:
  mutex __mut_;
  unsynchronized_pool_resource __unsync_;
};

pool_optionsはこれらのメモリプールの設定に使えます。

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

各チャンクに含めることができるブロック数の最大値を表します。メモリプールが上流のメモリリソースから一度に取得するメモリ量を制限するために使います。0に設定すると、カーネルはmunge_options関数を使ってpool_optionsを書き換えます。

  • largest_required_pool_block

メモリプールが扱える最大ブロックサイズを表します。

関連トピック

コメント