コンピュータサイエンスにおいて、領域ベースのメモリ管理とは、割り当てられた各オブジェクトを領域に割り当てるメモリ管理の一種です。領域は、パーティション、サブプール、ゾーン、アリーナ、エリア、またはメモリコンテキストとも呼ばれ、割り当て済みのオブジェクトの集合であり、一度に効率的に再割り当てまたは解放することができます。領域ベースの管理を使用するメモリ割り当て器は、エリア割り当て器と呼ばれることが多く、単一のポインタを「バンプ」するだけで動作する場合は、バンプ割り当て器と呼ばれます。
スタック割り当てと同様に、リージョンは低オーバーヘッドでメモリの割り当てと解放を容易にしますが、より柔軟性が高く、オブジェクトが割り当てられたスタックフレームよりも長く存続することができます。一般的な実装では、リージョン内のすべてのオブジェクトは、スタックフレームが通常割り当てられる方法と同様に、単一の連続したメモリアドレス範囲に割り当てられます。
OS/360 およびそれ以降のバージョンでは、この概念は 2 つのレベルで適用されます。各ジョブは連続したパーティション[ a ]またはリージョン[ b ]内で実行されます。ストレージ割り当て要求ではサブプールが指定され、アプリケーションはサブプール全体を解放できます。サブプールのストレージは、リージョンまたはパーティションから 2 KiB [ c ]または 4 KiB [ d ]の倍数であるブロックで割り当てられますが、通常は連続していません。
簡単な例として、コンパイル時のシンプルな環境と、文字列と整数値からなるシンプルなデータ型を実装した以下のC++コードを考えてみましょう。
import std ;using std :: bad_alloc ; using std :: constructible_from ; using std :: span ; using std :: string ; using std :: unique_ptr ;class Arena { private : unique_ptr < byte [] > storage ; size_t capacity ; size_t offset ;constexpr void * allocateBytes ( size_t size , size_t alignment ) { void * ptr = storage . get () + offset ; size_t space = capacity - offset ; size_t alignedOffset = capacity - space ; if ( alignedOffset + size > capacity ) { throw bad_alloc ( "バイトの割り当てに失敗しました!" ); } offset = alignedOffset + size ; return ptr ; } public : constexpr explicit Arena ( size_t capacity ) : storage { std :: make_unique < byte [] > ( capacity )}, capacity { capacity }, offset { 0 } {}constexpr ~ Arena () = default ; constexpr Arena ( Arena && ) noexcept = default ; constexpr Arena & operator = ( Arena && ) noexcept = default ;constexpr Arena ( const Arena & ) = delete ( "Arena のコピー構築が無効になっています" ); constexpr Arena & operator = ( const Arena & ) = delete ( "Arena のコピー代入が無効になっています" );template < typename T , typename ... Args > requires constructible_from < T , Args ... > constexpr T * allocate ( Args && ... args ) { void * mem = allocateBytes ( sizeof ( T ), alignof ( T )); return std :: construct_at ( static_cast < T *> ( mem ), std :: forward < Args > ( args )...); }constexpr void reset () noexcept { offset = 0 ; }constexpr span < byte > remaining () const noexcept { return { storage . get () + offset , capacity - offset }; } };struct Node { string name ; int value ; };int main () { Arena arena ( 4096 ); Node * a = arena . allocate < Node > ( "Alpha" , 1 ); Node * b = arena . allocate < Node > ( "Beta" , 2 );std :: println ( "a = ({}, {})" , a- > name , a- > value ); std :: println ( "b = ({}, {})" , b- > name , b- > value ) ; arena.reset ( ); }単純な明示的領域は簡単に実装できます。以下の説明は、Hanson の研究に基づいています。[ 1 ]各領域は、大きなメモリブロックのリンク リストとして実装されます。各ブロックは、多くの割り当てに対応できる十分な大きさである必要があります。現在のブロックは、ブロック内の次の空き位置へのポインタを保持し、ブロックがいっぱいになると、新しいブロックが割り当てられてリストに追加されます。領域が解放されると、次の空き位置へのポインタは最初のブロックの先頭にリセットされ、ブロックのリストは次に割り当てられる領域に再利用できます。あるいは、領域が解放されると、そのブロックのリストは、他の領域が後で新しいブロックを割り当てることができるグローバル フリー リストに追加できます。この単純なスキームのどちらの場合も、領域内の個々のオブジェクトを解放することはできません。
この方式では、割り当てられるバイトあたりの総コストが非常に低く、ほとんどの割り当ては比較と次の空き位置ポインタの更新のみで済みます。領域の解放は定数時間で実行でき、めったに行われません。一般的なガベージコレクションシステムとは異なり、データの型をタグ付けする必要はありません。
領域の基本概念は非常に古く、1967年にダグラス・T・ロスのAEDフリーストレージパッケージに初めて登場しました。このパッケージでは、メモリが階層的なゾーンに分割され、各ゾーンには独自の割り当て機能があり、ゾーンを一度にすべて解放できるため、ゾーンを領域として使用できます。[ 2 ] 1976年に、PL/I標準にAREAデータ型が追加されました。[ 3 ] 1990年に、ハンソンは、C言語の明示的な領域(彼がアリーナと呼んだもの)が、割り当てバイトあたりの時間パフォーマンスで、既知の最速のヒープ割り当てメカニズムよりも優れていることを実証しました。[ 1 ]明示的な領域は、 Apache HTTP Server(これをプールと呼ぶ)やPostgreSQLデータベース管理システム(これをメモリコンテキストと呼ぶ)など、初期のC言語ベースのソフトウェアプロジェクトの設計に役立ちました。 [ 4 ]従来のヒープ割り当てと同様に、これらのスキームはメモリの安全性を提供しません。プログラマーが、ダングリングポインタを介して解放された領域にアクセスしたり、領域の解放を忘れてメモリリークを引き起こしたりする可能性があります。
1988年、研究者たちは領域推論という概念を導入することで、安全なメモリ割り当てのために領域を利用する方法の研究を開始しました。領域推論とは、コンパイル時にコンパイラが領域の作成と解放、および個々の静的割り当て式を特定の領域に割り当てる処理を挿入するものです。コンパイラは、ダングリングポインタやメモリリークが発生しないように、この処理を実行できます。
RuggieriとMurtaghによる初期の研究[ 5 ]では、各関数の開始時に領域が作成され、終了時に解放されます。次に、データフロー解析を使用して各静的割り当て式の寿命を決定し、その寿命全体を含む最も若い領域に割り当てます。
1994年、この研究はTofteとTalpinによる画期的な研究で一般化され、型推論と多相領域型および領域計算の理論的概念に基づく異なるアルゴリズムを使用して、関数型プログラミング言語であるStandard MLで型多相性と高階関数をサポートするようになりました。[ 6 ] [ 7 ]彼らの研究では、領域を含むラムダ計算の拡張が導入され、2つの構成要素が追加されました。
この構文構造により、領域は入れ子になっているため、作成後また、解放される必要もあります結果として領域のスタックができます。さらに、領域は作成された関数内で解放する必要があります。これらの制約は Aiken らによって緩和されました[ 8 ]
この拡張ラムダ計算は、Standard ML プログラムをマシン コードにコンパイルするための、証明可能なメモリ安全中間表現として機能することを意図していましたが、大規模プログラムで良好な結果を生み出す翻訳器を構築するには、再帰呼び出し、末尾呼び出し、単一の値のみを含む領域の削除など、新しい分析で解決する必要のある多くの実際的な制限に直面しました。この作業は 1995 年に完了し[ 9 ]、ガベージ コレクションの代わりに領域割り当てに基づく ML のバージョンである ML Kit に統合されました。これにより、中規模のテスト プログラムで 2 つの直接比較が可能になり、プログラムの「領域フレンドリー」さに応じて、結果が大きく異なりました (「10 倍速いものから 4 倍遅いものまで」)。ただし、コンパイル時間は数分程度でした。[ 10 ] ML Kit は最終的に、モジュールの個別コンパイルのスキームと、領域推論とトレース ガベージ コレクションを組み合わせたハイブリッド テクニックという 2 つの追加により、大規模アプリケーションに拡張されました。[ 11 ] [ 12 ]
ML Kitの開発後、領域は他の言語環境にも一般化され始めた。
std::pmr::monotonic_buffer_resourceライブラリ内のC++ 機能std::pmr(ヘッダーで定義<memory_resource>)。[ 32 ]java.lang.foreign.Arena、、などのクラスが含まれていますjava.lang.foreign.MemorySegment。[ 35 ]arenaにはライブラリがあり、 arena.Arena. [ 36 ]std.heap.ArenaAllocator。[ 37 ]リージョンを使用するシステムでは、リージョンが解放される前に非常に大きくなり、大量のデッドデータが含まれるという問題が発生する可能性があります。これらは一般的に「リーク」と呼ばれます(最終的には解放されますが)。リークを解消するには、通常、寿命の短い新しいリージョンを導入することによってプログラムを再構築する必要があります。この種の問題のデバッグは、リージョン推論を使用するシステムでは特に困難です。プログラマは、問題の診断のために、基盤となる推論アルゴリズムを理解するか、冗長な中間表現を調べる必要があります。トレースガベージコレクタは、プログラムの変更なしにこの種のデータをタイムリーに解放するのに効果的です。これが、ハイブリッドリージョン/GCシステムの正当化の1つでした。[ 11 ]一方、トレースガベージコレクタでも、二度と使用されないデータへの参照が保持されている場合、微妙なリークが発生する可能性があります。
領域ベースのメモリ管理は、領域の数が比較的少なく、各領域に多数のオブジェクトが含まれている場合に最も効果を発揮します。疎な領域を多数含むプログラムでは、内部断片化が発生し、メモリの無駄遣いや領域管理の時間オーバーヘッドにつながります。繰り返しになりますが、領域推論が存在する場合、この問題の診断はより困難になる可能性があります。
前述のように、RC は領域と参照カウントのハイブリッドを使用し、領域内の参照は変更されてもカウントを更新する必要がないため、参照カウントのオーバーヘッドを制限します。同様に、マーク領域ハイブリッド方式の中には、トレースガベージコレクションと領域を組み合わせたものがあります。これらは、ヒープを領域に分割し、生存オブジェクトを含む領域をマークするマークスイープパスを実行し、マークされていない領域を解放することで機能します。これらの方式は、効果を維持するために継続的なデフラグメンテーションが必要です。[ 38 ]
{{cite journal}}: CS1メンテナンス: アーカイブサービスは非推奨になりました (リンク){{cite web}}: CS1 maint: 数値名: 著者リスト (リンク){{cite tech report}}: CS1 maint: 複数の名前: 著者リスト (リンク)