web-dev-qa-db-ja.com

動的メモリ割り当てヒープを実装するために使用されるデータ構造は何ですか?

ヒープ(データ構造) を使用して ヒープ(動的メモリ割り当て) を実装すると常に想定していましたが、間違っていると言われました。

ヒープ(たとえば、通常のmallocルーチンやWindowsのHeapCreateなどによって実装されるもの)は、通常、どのように実装されますか?彼らはどのようなデータ構造を使用していますか?

私がしていることではない質問:

オンラインで検索しているときに、ヒープの実装方法の説明がトンあるのを見ました厳しい制限付き
いくつか例を挙げると、実装方法の説明をたくさん見てきました。

  • メモリをOSに解放しないヒープ(!)
  • 小さい、同じサイズのブロックでのみ妥当なパフォーマンスを提供するヒープ
  • 大きくて連続したブロックに対してのみ妥当なパフォーマンスを提供するヒープ
  • 等.

そしてそれは面白いです、彼らはすべて難しい質問を避けます:
「通常の」汎用ヒープ(mallocHeapCreateの背後にあるものなど)はどのように実装されますか?

彼らはどのようなデータ構造(そしておそらくアルゴリズム)を使用していますか?

26
user541686

アロケータは非常に複雑になる傾向があり、実装方法が大きく異なることがよくあります。

1つの一般的なデータ構造またはアルゴリズムの観点からそれらを実際に説明することはできませんが、いくつかの一般的なテーマがあります。

  1. メモリは、システムから大きなチャンク(多くの場合、一度にメガバイト)で取得されます。
  2. これらのチャンクは、割り当てを実行するときにさまざまな小さなチャンクに分割されます。割り当てるサイズとまったく同じではありませんが、通常は特定の範囲(200〜250バイト、251〜500バイトなど)です。これは多層である場合があり、実際のリクエストの前に「中程度のチャンク」の追加レイヤーがあります。
  3. どの「大きなチャンク」から断片を切り離すかを制御することは、非常に困難で重要なことです。これは、メモリの断片化に大きく影響します。
  4. これらの範囲ごとに、1つ以上の空きプール(別名「空きリスト」、「メモリプール」、「ルックアサイドリスト」)が維持されます。時にはスレッドローカルプールですら。これにより、同じサイズの多くのオブジェクトの割り当て/割り当て解除のパターンを大幅に高速化できます。
  5. 大規模な割り当ては、多くのRAMを無駄にせず、プールされないようにするために、少し異なる方法で処理されます。

ソースコードをチェックしたい場合、 jemalloc は最新の高性能アロケーターであり、他の一般的なアロケーターの複雑さを代表するものでなければなりません。 TCMalloc は、もう1つの一般的な汎用アロケーターであり、そのWebサイトではすべての厄介な実装の詳細が説明されています。 Intelの スレッドビルディングブロック には、高い同時実行性のために特別に構築されたアロケータがあります。

Windowsと* nixの間には興味深い違いが1つあります。 * nixでは、アロケーターはアプリが使用するアドレス空間を非常に低レベルで制御します。 Windowsでは、基本的に、独自のアロケータのベースとなる、コースグレインの低速アロケータVirtualAllocがあります。

これにより、* nix互換のアロケーターは、通常、malloc/freeの実装を直接提供します。この実装では、すべてに1つのアロケーターのみを使用すると想定されます(そうでない場合は、互いに踏みにじられます)。一方、Windows固有のアロケーターは、malloc/freeをそのままにして、追加の機能を提供し、調和して使用できます(たとえば、HeapCreateを使用して、他のユーザーと一緒に機能するプライベートヒープを作成できます)。

実際には、この柔軟性のトレードにより、* nixアロケーターはパフォーマンス面で小さな足がかりになります。アプリがWindowsで意図的に複数のヒープを使用するのを見るのは非常にまれです-ほとんどの場合、それぞれが独自のmalloc/freeを持つ異なるランタイムを使用する異なるDLLが原因で、多くの原因となる可能性があります。どのヒープからメモリが発生したかを追跡することに熱心でない場合は、頭痛の種になります。

14
Cory Nelson

注:次の回答は、仮想メモリを備えた一般的な最新のシステムを使用していることを前提としています。 CおよびC++標準は仮想メモリを必要としません。したがって、もちろん、この機能がないハードウェアでは、このような仮定に頼ることはできません(たとえば、GPUには通常この機能がなく、PICのような非常に小さなハードウェアもありません)。


これは、使用しているプラ​​ットフォームによって異なります。ヒープは非常に複雑な獣になる可能性があります。単一のデータ構造のみを使用するわけではありません。また、「標準」のデータ構造はありません。ヒープコードが配置されている場所も、プラットフォームによって異なります。たとえば、ヒープコードは通常、UnixボックスのCランタイムによって提供されます。ただし、通常はWindowsのオペレーティングシステムによって提供されます。

  1. はい、これはUnixマシンでは一般的です。 * nixの基盤となるAPIとメモリモデルの動作方法が原因です。基本的に、これらのシステムのオペレーティングシステムにメモリを返す標準APIでは、ユーザーメモリが割り当てられる場所とユーザーメモリとスタックなどのシステム機能の間の「穴」の間の「フロンティア」でのみメモリを返すことができます。 (問題のAPIは brkまたはsbrk です)。多くのヒープは、メモリをオペレーティングシステムに戻す代わりに、プログラムが適切に使用しなくなったメモリを再利用しようとするだけであり、メモリをシステムに戻そうとはしません。 sbrkVirtualAlloc)に相当するものにはこの制限がないため、これはWindowsではあまり一般的ではありません。 (ただし、sbrkと同様に、非常に高価であり、ページサイズとページ揃えのチャンクのみを割り当てるなどの注意点があります。したがって、ヒープはできるだけまれに呼び出しを試みます)
  2. これは、メモリを固定サイズのチャンクに分割し、空きチャンクの1つを返す「ブロックアロケータ」のように聞こえます。私の(限定的ではありますが)理解すると、WindowsのRtlHeapは、さまざまな既知のブロックサイズに対して、このような多数のデータ構造を維持しています。 (たとえば、サイズ16のブロック用に1つあります)RtlHeapはこれらを「ルックアサイドリスト」と呼びます。
  3. このケースをうまく処理する特定の構造を私は本当に知りません。大きなブロックは、アドレス空間の断片化を引き起こすため、ほとんどの割り当てシステムで問題があります。

主要なプラットフォームで採用されている一般的な割り当て戦略について説明している私が見つけた最良の参考資料は、Robertによる本 CおよびC++でのセキュアコーディング)です。シーコード 。第4章はすべて、ヒープデータ構造(およびユーザーが上記のヒープシステムを誤って使用した場合に発生する問題)に特化しています。

6
Billy ONeal