標準ライブラリコンテナのメモリ割り当て子:allocator

カスタムディストリビューターは、パフォーマンスを向上させ、メモリ使用効率を高め、頻繁な少量のメモリ割り当ての問題を解決できます。

前因

近頃、ネットワークパケットの開発に携わり、頻繁に小さなメモリ領域を申請し解放する必要があり、当初はメモリプールを使用することを検討していました。いくつかの既存のメモリプールを確認したところ、この https://github.com/cacay/MemoryPool を見つけました。インターフェースを見たとき、このメモリプールの実装が少し奇妙だと疑問に思いました。「MemoryPool」の実装ロジックは、固定サイズのメモリ領域を申請することです。boostのメモリプールインターフェースを見てみると、テンプレートを提供し、使用時にインスタンス化します。ちょうどこのライブラリには、allocatorという概念について言及した記事があります。

#### [wiki](https://zh.wikipedia.org/wiki/%E5%88%86%E9%85%8D%E5%99%A8_(C%2B%2B))

C++プログラミングにおいて、割り当て子(英語:allocator)はC++標準ライブラリの重要な構成要素です。C++のライブラリには、リスト、集合などのように、さまざまな「コンテナ」と呼ばれるデータ構造が定義されており、これらのコンテナの共通の特徴は、プログラムの実行時にサイズを変更できることです。この機能を実装するために、動的メモリ割り当てが必要となります。割り当て子は、これらのコンテナがメモリへの割り当てと解放のリクエストを処理するために使用されます。言い換えれば、割り当て子は、標準テンプレートライブラリ(STL)コンテナのメモリ管理に関する低レベルの詳細をカプセル化します。

割り当て子は、アレクサンドル・ステパノフによってC++標準テンプレートライブラリ(STL)の一部として最初に発明されました。その目的は、「ライブラリをより柔軟にし、低レベルのデータモデルに独立した方法で利用できるようにする」ことであり、プログラマがライブラリ内でカスタムポインタや参照型を使用することを可能にするものでした。ただし、標準テンプレートライブラリをC++標準に組み込む際、C++標準委員会は、完全なデータモデル抽象化処理が不可受容なパフォーマンス低下をもたらすことを認識しました。そのため、妥協策として、割り当て子の制限がより厳しくなり、ステパノフの当初の構想と比較して、現在の標準で記述されている割り当て子のカスタマイズ性は大幅に制限されています。

割り当て子のカスタマイズは制限されていますが、多くの状況ではカスタム割り当て子が必要となります。これは通常、異なる種類のメモリ空間(共有メモリと回収されたメモリなど)へのアクセス方法をカプセル化したり、メモリプールを使用したメモリ割り当てのパフォーマンスを向上させたりするために行われます。さらに、メモリ使用量と実行時間から見ると、頻繁に少量のメモリを割り当てるプログラムでは、専用の割り当て子を作成することで利益を得ることができます。
#### [使用需求](https://zh.wikipedia.org/wiki/%E5%88%86%E9%85%8D%E5%99%A8_(C%2B%2B))

カスタムアロケータの主な理由は性能向上です。専用のアロケータを使用することで、プログラムのパフォーマンスを向上させたり、メモリ使用量を削減したり、あるいは両方を組み合わせることも可能です[4][8]。デフォルトのアロケータは`new`演算子を使用してストレージスペースを割り当てるため、これは通常C言語のヒープ割り当て関数(`malloc()`)によって実装されます[9]。ヒープ割り当て関数は、偶発的な大量メモリ割り当てを最適化するように設計されているため、ベクトルや双端キューなどの、一度に大量のメモリを必要とするコンテナにメモリを割り当てる場合は、デフォルトのアロケータは通常効率的です[8]。しかし、連想コンテナと双方向リストのような、頻繁に少量メモリを割り当てて解除するコンテナの場合、デフォルトのアロケータを使用すると、通常効率が低下します[4][9]。さらに、`malloc()`に基づくデフォルトのアロケータには、より悪い参照局所性や、メモリの断片化を引き起こす可能性があるなど、多くの問題があります[4][9]。

要するに、このセクション(……)(まるで)は、この標準におけるアロケータに関する「夢を見た」のスピーチです。夢が実現する前に、移植性を重視するプログラマーは、ステートレスなカスタムアロケータを使用することになります。
——スコット・メイエス,《Effective STL》

上記を踏まえ、メモリ割り当ての頻度が多い場合に、メモリプールベースのアロケータを使用して問題を解決することがよくあります[8]。オンデマンド割り当てとは異なり、メモリプールベースのアロケータを使用する場合、プログラムは事前に大きなブロックのメモリ(つまり「メモリプール」)を割り当て、次にメモリを割り当てる必要がある場合、カスタムアロケータは、リクエスト元にプール内のメモリへのポインタを返します。オブジェクトが破棄される際には、実際の割り当て解除を行うのではなく、メモリプールのライフサイクルが終了するまで遅延させます[注 1][8]。

「カスタムアロケータ」というトピックに関しては、多くのC++専門家や著者がこの分野で議論しており、スコット・メイエス著の《Effective STL》やアンデル・アレクサンドレスク著の《Modern C++ Design》に言及しています。メイエスは、特定の型`T`のアロケータのすべてのインスタンスが等しいという要件を満たす移植可能なアロケータのインスタンスにはステートを含めない必要があると洞察しており、ステートレスなアロケータの使用を推奨しています。C++標準はライブラリの実装者が状態を含むアロケータをサポートするように奨励していますが、メイエスは「このセクションは、(まるで)素晴らしい見方ですが、ほとんど空言であり」、アロケータの制約は「過度に厳格」であると述べています[4]。たとえば、STLの`list`は`splice`メソッドをサポートしており、これは1つのリストオブジェクト`A`のノードが別のリストオブジェクト`B`に直接挿入されることを意味し、`A`のアロケータによって割り当てられたメモリが`B`のアロケータによって解放される必要があるため、`A`と`B`のアロケータインスタンスは等しいことが導き出されます。メイセスの結論は、アロケータはステートレスな静的メソッドの型として定義するのが最善であるということです。たとえば、C++標準では、アロケータは`rebind`メソッドを実装するその他のクラステンプレートを持つ必要があります。

さらに、ヤン・ストローストルップ著『C++プログラミング言語』では、「割り当てを厳密に制限し、各オブジェクトの情報を異なるようにすることについては、明らかに問題ありません」(大意)と述べており、ほとんどのアロケータはステートを持たず、ステートを持たない場合でもパフォーマンスが向上するという意見を示しています。彼は、メモリプール型アロケータ、共有メモリ型アロケータ、ガベージコレクション型アロケータの3種類のカスタムアロケータを提案し、内部メモリプールを使用して少量メモリを高速に割り当て/解除するアロケータの実装を示しました[3]。ただし、このような最適化は、彼の提供したサンプルアロケータで既に実現されている可能性があると彼は指摘しています。

カスタムアロケータのもう1つの用途は
Licensed under CC BY-NC-SA 4.0
最終更新 2025年06月02日 20:54
金融ITプログラマーのいじくり回しと日常のつぶやき
Hugo で構築されています。
テーマ StackJimmy によって設計されています。