一般的にシステムがスタックを廃止し、メモリ管理にヒープを使用する方が効率的ですか?


14

スタックで実行できることはすべてヒープで実行できるようですが、ヒープで実行できるすべてがスタックで実行できるわけではありません。あれは正しいですか?次に、簡単にするために、特定のワークロードでパフォーマンスが少し低下したとしても、1つの標準(つまり、ヒープ)を使用する方が良いのではないでしょうか。

モジュール性とパフォーマンスのトレードオフを考えてください。これはこのシナリオを説明する最善の方法ではないことを知っていますが、一般的に、パフォーマンスが向上する可能性がある場合でも、理解と設計の単純さはより良い選択肢になると思われます。


1
CおよびC ++では、ヒープに割り当てられたメモリを明示的に割り当て解除する必要があります。それは簡単ではありません。
user16764

C#の実装を使用して、スタックオブジェクトがひどいガベージコレクションのあるヒープのような領域に割り当てられていることをプロファイリングで明らかにしました。私の解決策は?移動はすべて永続ヒープメモリへの可能性(例えばループ変数、一時変数など)。プログラムがRAMの10倍を消費し、10倍の速度で実行するようにしました。
imallett

@IanMallett:問題と解決策についてのあなたの説明がわかりません。どこかに詳細情報のリンクがありますか?通常、スタックベースの割り当ての方が高速であることがわかります。
フランクヒルマン14年

@FrankHilemanの基本的な問題はこれでした。私が使用していたC#の実装は、ガベージコレクションの速度が非常に悪かったのです。「解決策」は、実行時にメモリ操作が発生しないようにすべての変数を永続化することでした。しばらく前に、C#/ XNAの開発全般についての意見を書きましたがこれもいくつかのコンテキストについて説明しています。
イマレット14年

@IanMallett:ありがとう。最近C#を主に使用している元のC / C ++開発者として、私の経験はかなり異なっていました。ライブラリが最大の問題だと思います。XBox360プラットフォームは.net開発者にとって中途半端だったようです。通常、GCに問題がある場合は、プールに切り替えます。それは役立ちます。
フランクヒルマン14年

回答:


30

ヒープは、高速のメモリ割り当てと割り当て解除が苦手です。限られた期間で多くの小さなメモリを取得したい場合、ヒープは最良の選択ではありません。非常に単純な割り当て/割り当て解除アルゴリズムを備えたスタックは、これに自然に優れています(ハードウェアに組み込まれている場合はさらにそうです)。これが、関数に引数を渡したりローカル変数を保存するなどの目的でそれを使用する理由です重要な欠点は、スペースが限られていることです。そのため、その中に大きなオブジェクトを保持したり、長寿命のオブジェクトに使用しようとしたりすることは、どちらも悪い考えです。

プログラミング言語を単純化するためにスタックを完全に取り除くのは間違った方法ですIMO-より良いアプローチは、違いを取り除き、コンパイラが使用するストレージの種類を把握できるようにすることです。人間の考え方に近いレベルの構造-実際、C#、Java、Pythonなどの高レベル言語はまさにこれを行います。それらは、ヒープに割り当てられたオブジェクトとスタックに割り当てられたプリミティブ(.NET言語の「参照型」と「値型」)に対してほぼ同一の構文を提供します。正しく(ただし、スタックとヒープが内部でどのように機能するかを実際に知る必要はありません)。


2
これは良かったです:)本当に簡潔で初心者にとって有益です!
ダークテンプラー

1
多くのCPUでは、スタックはハードウェアで処理されます。これは言語外の問題ですが、実行時に大きな役割を果たします。
パトリックヒューズ

@Patrick Hughes:はい、しかし、ヒープもハードウェアにもありますよね?
ダークテンプラー

@Dark Patrickが言いたいのは、x86のようなアーキテクチャには、スタックを管理するための特別なレジスタと、スタックに何かを追加または削除するための特別な命令があるということです。それは非常に高速になります。
FUZxxl

3
@Donal Fellows:すべて正しい。しかし、ポイントは、スタックとヒープの両方に長所と短所があり、それに応じてそれらを使用すると最も効率的なコードが得られることです。
tdammers

8

簡単に言えば、スタックは少しのパフォーマンスではありません。ヒープよりも数百または数千倍高速です。さらに、最新のマシンのほとんどは、スタック(x86など)のハードウェアサポートを備えており、コールスタックなどのハードウェア機能は削除できません。


最新のマシンにはスタックのハードウェアサポートがあると言うとき、どういう意味ですか?スタック自体はすでにハードウェア内にありますよね?
ダークテンプラー

1
x86には、スタックを処理するための特別なレジスタと命令があります。x86はヒープをサポートしていません-そのようなものはOSによって作成されます。
Pubby

8

番号

C ++のスタック領域は、比較すると非常に高速です。経験豊富なC ++開発者がその機能を無効にすることはできません。

C ++では、選択と制御が可能です。設計者は、実行時間またはスペースを大幅に追加する機能を導入することを特に好みませんでした。

その選択を行使する

すべてのオブジェクトを動的に割り当てる必要があるライブラリまたはプログラムを構築する場合は、C ++で実行できます。それは比較的ゆっくり実行されますが、その「モジュール性」を持つことができます。私たちの残りの部分では、モジュール性は常にオプションであり、必要に応じて導入します。両方とも優れた/高速な実装に必要であるためです。

代替案

各オブジェクトのストレージをヒープ上に作成する必要がある他の言語があります。それは非常に遅く、そのため、両方を学ぶ必要がある(IMO)よりも悪い方法で設計(実世界のプログラム)を危険にさらします。

両方が重要であり、C ++はそれぞれのシナリオで効果的に両方を使用できるようにします。とはいえ、OPのこれらの要素が重要である場合(たとえば、より高いレベルの言語で読む場合)、C ++言語は設計にとって理想的ではない場合があります。


ヒープは実際にはスタックと同じ速度ですが、割り当てのための特別なハードウェアサポートはありません。一方、ヒープを大幅に高速化する方法があります(多くの条件により、それらはエキスパート専用のテクニックになります)。
ドナルドフェローズ

@DonalFellows:スタックのハードウェアサポートは無関係です。重要なのは、何かがリリースされるたびに、その後に割り当てられたものをリリースできることを知ることです。一部のプログラミング言語には、オブジェクトを独立して解放できるヒープはありませんが、代わりに「後に割り当てられるすべてを解放する」メソッドしかありません。
supercat

6

次に、簡単にするために、特定のワークロードでパフォーマンスが少し低下したとしても、1つの標準(つまり、ヒープ)を使用する方が良いのではないでしょうか。

実際、パフォーマンスへの影響はかなり大きいでしょう!

他の人が指摘したように、スタックは、LIFO(後入れ先出し)ルールに従うデータを管理するための非常に効率的な構造です。スタック上のメモリの割り当て/解放は、通常、CPU上のレジスタを変更するだけです。レジスタの変更は、ほとんどの場合、プロセッサが実行できる最速の操作の1つです。

通常、ヒープはかなり複雑なデータ構造であり、メモリの割り当て/解放には、関連するすべてのブックキーピングを実行するために多くの命令が必要です。さらに悪いことに、一般的な実装では、ヒープを操作するためのすべての呼び出しは、オペレーティングシステムへの呼び出しになる可能性があります。オペレーティングシステムの呼び出しは非常に時間がかかります!通常、プログラムはユーザーモードからカーネルモードに切り替える必要があり、これが発生するたびに、オペレーティングシステムは他のプログラムにもっと差し迫ったニーズがあると判断し、プログラムを待つ必要があると判断する場合があります。


5

Simulaはすべてにヒープを使用しました。ヒープにすべてを置くと、ローカル変数の間接的なレベルが常に1つ高くなり、ガベージコレクターに追加のプレッシャーがかかります(ガベージコレクターが本当に吸い込んだことを考慮する必要があります)。それが、BjarneがC ++を発明した理由の一部です。


基本的に、C ++はヒープも使用するだけですか?
ダークテンプラー

2
@ダーク:何?いいえ。Simulaにスタックがないことは、それを改善するためのインスピレーションでした。
fredoverflow

ああ、あなたの今の意味がわかりました!ありがとう+1 :)
ダークテンプラー

3

スタックは、たとえば関数呼び出しに関連付けられたメタデータなどのLIFOデータに対して非常に効率的です。スタックは、CPU固有の設計機能も活用します。このレベルでのパフォーマンスは、プロセス内の他のほぼすべての基本であるため、そのレベルでその「小さな」ヒットを取得すると、非常に広く伝播します。さらに、ヒープメモリはOSによって移動可能であり、スタックにとって致命的です。スタックはヒープに実装できますが、オーバーヘッドが必要です。オーバーヘッドは、最も詳細なレベルでプロセスの各部分に文字通り影響します。


2

コードを書くという点では「効率的」かもしれませんが、ソフトウェアの効率という点では確かではありません。スタックの割り当ては基本的に無料です(スタックポインターを移動し、ローカル変数用にスタック上のスペースを予約するのに、ほんの数個のマシン命令が必要です)。

スタックの割り当てにはほとんど時間がかからないため、非常に効率的なヒープでも割り当ては100k(1M +ではない場合)倍遅くなります。

ここで、典型的なアプリケーションが使用するローカル変数およびその他のデータ構造の数を想像してください。ループカウンタとして使用するすべての小さな「i」は、100万倍遅く割り当てられます。

ハードウェアの速度が十分であれば、ヒープのみを使用するアプリケーションを作成できます。しかし、現在、ヒープを利用して同じハードウェアを使用した場合にどのようなアプリケーションを作成できるかをイメージしています。


「典型的なアプリケーションが使用するローカル変数やその他のデータ構造の数を想像してください」と言うとき、他にどのようなデータ構造を具体的に参照していますか?
ダークテンプラー

1
値「100k」および「1M +」は何らかの形で科学的ですか?それとも、単に「たくさん」と言う方法ですか?
ブルーノレイズ

@Bruno-私が使用した100Kと1Mの数字は、実際にはポイントを証明するための控えめな見積もりです。VSとC ++に精通している場合は、スタックに100バイトを割り当てるプログラムを作成し、ヒープに100バイトを割り当てるプログラムを作成します。次に、逆アセンブリビューに切り替えて、各割り当てにかかるアセンブリ命令の数を単純にカウントします。ヒープ操作は通常、Windows DLLへの複数の関数呼び出しであり、バケットとリンクリストがあり、さらに合体やその他のアルゴリズムがあります。スタックを使用すると、1つのアセンブリ命令「espを追加、100」に要約できます
...-DXM

2
「100k(1M +でない場合)倍遅い」?それはかなり誇張されています。2桁、おそらく3桁遅くしますが、それだけです。少なくとも、私のLinuxは、コアi5で6秒未満で1億個のヒープ割り当て(+周囲の命令)を行うことができます。これは、割り当てごとに数百命令を超えることはできません。それはだ場合は6スタックよりも遅い桁違いにOSのヒープ実装を持つすぎなかっ何か問題があります。確かに、Windowsには多くの間違いがありますが、それは
...-leftaroundabout

1
モデレーターはおそらくこのコメントスレッド全体を殺そうとしています。だからここに契約がある、私は実際の数字が私のから引き出されたことを認める....しかし、要因が本当に、本当に大きく、それ以上コメントをしないことに同意しよう:)
DXM

2

「ガベージコレクションは高速ですが、スタックは高速です」に興味があるかもしれません。

http://dspace.mit.edu/bitstream/handle/1721.1/6622/AIM-1462.ps.Z

私がそれを正しく読んだ場合、これらの人はヒープに「スタックフレーム」を割り当てるようにCコンパイラを変更し、スタックをポップする代わりにガベージコレクションを使用してフレームの割り当てを解除します。

スタックに割り当てられた「スタックフレーム」は、ヒープに割り当てられた「スタックフレーム」よりも断然優れています。


1

呼び出しスタックはヒープでどのように機能しますか?基本的に、すべてのプログラムでヒープにスタックを割り当てる必要があるので、OS +ハードウェアでそれを行わないのはなぜですか?

本当にシンプルで効率的なものにしたい場合は、ユーザーにメモリのチャンクを与えて、それに対処させてください。もちろん、自分のすべてを実装したい人はいないので、スタックとヒープがあります。


厳密に言うと、「呼び出しスタック」は、プログラミング言語ランタイム環境の必須機能ではありません。例えば、グラフ削減(遅延コーディング)によって遅延評価された関数型言語の単純な実装(これはコーディング済み)には呼び出しスタックがありません。ただし、呼び出しスタックは、特に最新のプロセッサがそれを使用することを想定しており、その使用に最適化されているため、非常に広く有用で広く使用されている手法です。
ベン

@Ben-言語からメモリ割り当てのようなものを抽象化するのは本当ですが(そして良いことですが)、これは現在普及しているコンピューターアーキテクチャを変更しません。したがって、グラフ削減コードは、実行中にスタックを使用します-好きかどうかにかかわらず。
インゴ

@Ingo実際には意味のある意味ではありません。確かに、OSは伝統的に「スタック」と呼ばれるメモリのセクションを初期化し、それを指すレジスタがあります。ただし、ソース言語の関数は、呼び出し順序でスタックフレームとして表されません。関数の実行は、ヒープ内のデータ構造の操作によって完全に表されます。最終呼び出しの最適化を使用しなくても、「スタックをオーバーフローさせる」ことはできません。これが、「呼び出しスタック」に基本的なものは何もないと言うときのことです。
ベン

私はソース言語の機能についてではなく、実際にグラフ縮小を実行するインタープリター(または何でも)の機能について話します。それらにはスタックが必要です。現代のハードウェアはグラフの縮小を行わないため、これは明らかです。したがって、グラフ削減アルゴリズムは最終的にマシンodeにマップされ、それらの中にサブルーチン呼び出しがあることに間違いはありません。QED。
インゴ

1

スタックとヒープの両方が必要です。これらは、たとえば次のようなさまざまな状況で使用されます。

  1. ヒープ割り当てには、sizeof(a [0])== sizeof(a [1])という制限があります
  2. スタック割り当てには、sizeof(a)がコンパイル時定数であるという制限があります
  3. ヒープ割り当てにより、ループ、グラフなどの複雑なデータ構造を作成できます
  4. スタック割り当てにより、コンパイル時のサイズのツリーを実行できます
  5. ヒープには所有権の追跡が必要です
  6. スタックの割り当てと割り当て解除は自動です
  7. ヒープメモリは、ポインターを介してあるスコープから別のスコープに簡単に渡すことができます。
  8. スタックメモリは各関数に対してローカルであり、オブジェクトを有効期間を延ばすために上位スコープに移動する必要があります(またはメンバー関数内ではなくオブジェクト内に格納されます)
  9. ヒープはパフォーマンスに悪い
  10. スタックはかなり速い
  11. ヒープオブジェクトは、所有権を取得するポインターを介して関数から返されます。またはshared_ptrs。
  12. スタックオブジェクトは、所有権を持たない参照を介して関数から返されます。
  13. ヒープでは、すべての新規を正しい種類の削除または削除[]と一致させる必要があります
  14. スタックオブジェクトはRAIIとコンストラクター初期化リストを使用します
  15. ヒープオブジェクトは関数内の任意のポイントで初期化でき、コンストラクターパラメーターを使用できません
  16. スタックオブジェクトは初期化にコンストラクターパラメーターを使用します
  17. ヒープは配列を使用し、配列サイズは実行時に変更できます
  18. スタックは単一オブジェクト用であり、サイズはコンパイル時に固定されます

基本的に、多くの詳細が異なるため、メカニズムはまったく比較できません。それらに共通する唯一のことは、どちらも何らかの方法でメモリを処理することです。


1

最近のコンピューターには、大型であるが低速なメインメモリシステムに加えて、キャッシュメモリのいくつかの層があります。メインメモリシステムから1バイトを読み書きするのに必要な時間で、最速のキャッシュメモリに数十回アクセスできます。したがって、1つの場所に1,000回アクセスする方が、1,000個(または100個)の独立した場所に1回アクセスするよりもはるかに高速です。ほとんどのアプリケーションは、スタックの最上部付近で少量のメモリの割り当てと割り当て解除を繰り返し行うため、スタックの最上部の場所は膨大な量(通常のアプリケーションでは99%以上)が使用され、再利用されますスタックアクセスのキャッシュメモリを使用して処理できます。

対照的に、アプリケーションが継続情報を格納するためにヒープオブジェクトを繰り返し作成および破棄する場合、これまでに作成されたすべてのスタックオブジェクトのすべてのバージョンをメインメモリに書き出す必要があります。CPUが開始したキャッシュページをリサイクルしたいと思った時点で、そのようなオブジェクトの大半が完全に役に立たなかったとしても、CPUはそれを知る方法がありません。その結果、CPUは無駄な情報の遅いメモリ書き込みを実行するために多くの時間を浪費しなければなりません。正確な速度のレシピではありません。

考慮すべきもう1つの点は、多くの場合、ルーチンに渡されたオブジェクト参照は、ルーチンが終了すると使用されないことを知っておくと役立つことです。パラメータとローカル変数がスタックを介して渡され、ルーチンのコードを調べて、渡された参照のコピーが保持されていないことが判明した場合、ルーチンを呼び出すコードは、オブジェクトは呼び出しの前に存在し、その後は何も存在しません。対照的に、パラメータがヒープオブジェクトを介して渡された場合、「ルーチンが戻った後」などの概念はやや曖昧になります。コードが継続のコピーを保持している場合、ルーチンは複数回続けて「戻る」ことができるためですシングルコール。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.