web-dev-qa-db-ja.com

ヒープでのメモリ割り当てがスタックよりもはるかに遅いのはなぜですか?

私はこれを何度も言われました。しかし、なぜかわかりません...ヒープからメモリを割り当てるときに追加のコストはどのくらいかかりますか?ハードウェアに関連していますか? CPUサイクルと関係がありますか?非常に多くの推測がありますが、正確な答えはありません...誰かが私にいくつかの詳細を教えてもらえますか?

「アンワインド」が言ったように、ヒープデータ構造はスタックよりも複雑です。そして、私の意見では、ヒープがプロセス内のすべてのスレッドによって共有されている間、一部のメモリスペースはスレッドの実行開始時にスタックとしてスレッドに割り当てられます。このパラダイムには、ガベージコレクションなど、共有ヒープの各スレッドの使用を管理するための追加のメカニズムが必要です。私はこれで正しいですか?

45
smwikipedia

ヒープはスタックよりもはるかに複雑なデータ構造であるためです。

多くのアーキテクチャでは、スタックにメモリを割り当てるのは、スタックポインタを変更するだけです。つまり、1つの命令です。ヒープにメモリを割り当てるには、十分な大きさのブロックを探して分割し、free()などを別の順序で実行できる「簿記」を管理する必要があります。

スタックに割り当てられたメモリは、スコープ(通常は関数)が終了したときに割り当て解除されることが保証されており、その一部だけを割り当て解除することはできません。

52
unwind

Unwindの答えを言い換える編集では、「ヒープデータ構造」について言及します。 heap として知られるデータ構造は、動的メモリ割り当てとは関係がないため、十分に注意してください。明確にするために、私は無料ストアのより多くの言語の弁護士用語を使用します。

すでに指摘したように、スタックの割り当てにはポインタをインクリメントする必要があります。これは通常、ほとんどのアーキテクチャに専用のレジスタがあり、割り当て解除には同じ量の作業が必要です。スタック割り当ても特定の機能にスコープされます。これにより、スタックに必要な合計スペースを事前に計算したり、スタックフレーム全体に対して単一の増分を実行したりするなど、コンパイラの最適化の候補としてはるかに適しています。同様に、スタックはデータの局所性がより保証されています。スタックの最上位は、ほとんどの場合、キャッシュライン内にあることが保証されており、すでに述べたように、スタックポインタは通常レジスタに格納されます。一部のアーキテクチャでコンパイラを最適化すると、より深いスタックフレームで呼び出された関数に引数として渡される以前のスタックフレームからの引数を再利用することで、スタック上の割り当てを完全に排除することもできます。同様に、スタックに割り当てられた変数は、割り当てを回避してレジスタに昇格できることがよくあります。

対照的に、無料ストアはmuchより複雑です。ガベージコレクションシステムについてはまったく別のトピックであるため、ここでは取り上げません。この質問はC言語について尋ねられました。通常、フリーストアからの割り当てと割り当て解除には、フリーリストやブロックプールなどのいくつかの異なるデータ構造が含まれます。これらのデータ構造と簿記にもメモリが必要であるため、そのスペースが無駄になります。さらに、簿記の記録は割り当てと混ざり合うことが多く、したがって他の割り当てのデータの局所性を損ないます。フリーストアからの割り当てには、基盤となるオペレーティングシステムに、通常は何らかの形式のスラブアロケータからより多くのプロセスメモリを要求することが含まれる場合があります。

簡単に比較するために、jemalloc-2.2.5とsloccountの数値を参照として使用すると、jemallocの実装にはC言語の8,800行を超えるソースコードと700行を超えるテストコードが含まれます。これにより、フリーストア割り当てとスタック割り当ての複雑さの違い(数千行のCコードと1つの命令)がわかります。

さらに、無料ストアの割り当ては単一の字句スコープに限定されないため、すべての割り当ての存続期間を追跡する必要があります。同様に、これらの割り当てはスレッド間で渡される可能性があるため、スレッド同期の問題が問題領域に入ります。フリーストアの割り当てに関するもう1つの大きな問題は、断片化です。断片化は多くの問題を引き起こします:

  • 断片化はデータの局所性を損ないます。
  • 断片化はメモリを浪費します。
  • 断片化により、大規模な割り当てのための空き領域を見つける作業が困難になります。

最近のシステムでは、スタックはフリーストアと比較して比較的小さいことが多いため、最終的にフリーストアはより多くのスペースを管理し、より困難な問題に取り組んでいます。また、スタックサイズの制限により、フリーストアは通常、より大きな割り当てに使用されます。非常に大きな割り当てと非常に小さな割り当ての両方を処理する必要があるというこの不一致により、フリーストアの作業も困難になります。通常、スタックの割り当ては数キロバイト以下のオーダーで小さく、スタックの合計サイズはわずか数メガバイトです。フリーストアは通常、プログラム内で残りのプロセススペース全体が与えられます。最近のマシンでは、これは数百ギガバイトになる可能性があり、無料のストア割り当てのサイズが、短い文字列のような数バイトからメガバイト、さらにはギガバイトの任意のデータまで変化することも珍しくありません。これは、フリーストアアロケータが基盤となるオペレーティングシステムの仮想メモリ管理を処理する必要があることを意味します。スタック割り当ては、基本的にコンピュータハードウェアに組み込まれています。

無料のストア割り当てについて本当に学びたい場合は、さまざまなmallocの実装について公開されている多くの論文や記事のいくつかを読んだり、コードを読んだりすることを強くお勧めします。開始するためのリンクは次のとおりです。

  • dlmalloc --Doug Leaのmalloc、ある時点でGNU C++で使用された履歴参照malloc実装
  • phkmalloc -VarnishWebキャッシュのPoul-HenningKamp作者によって書かれたmallocのFreeBSD実装
  • tcmalloc -一部のGoogle開発者によって実装されたスレッドキャッシングMalloc
  • jemalloc --FreeBSD用のJason Evanのmalloc実装(phkmallocの後継)

Tcmalloc実装の説明を含むいくつかの追加リンクを次に示します。

25
b4hand

スタックとヒープの主な違いは、スタック上のアイテムを順不同で削除できないことです。アイテムA、B、Cをスタックに追加する場合、最初にCを削除せずにBを削除することはできません。つまり、新しいアイテムをスタックに追加するということは、常にそれをスタックのendに追加することを意味します。これは、非常に簡単な操作です。スタックの最後を指すポインタを移動するだけです。

一方、ヒープでは、canアイテムを順不同で削除します。そして、後でメモリ内で他のアイテムを移動しない限り(一部のガベージコレクションヒープのように)、ヒープの中央に「穴」があります。つまりA、B、Cをヒープに追加してBを削除すると、ヒープはメモリ内で次のようになります。A_ Cここで、_は未使用の(空き)メモリのブロックです。ここで新しいアイテムDを追加する場合、アロケータはDに適合するのに十分な大きさの連続空き領域を見つける必要があります。メモリ内の連続空き領域の数によっては、これはコストのかかる操作になる可能性があります。また、ほとんどの場合、スタックの「最後の要素」ポインタを移動するよりもコストがかかります。

13
Niki