質問

プログラミング言語の本では、値型は スタック, 、参照型は ヒープ, 、これら 2 つが何であるかを説明せずに。これについての明確な説明を読んだことがありません。わかりました スタック は。しかし、

  • それらは (実際のコンピューターのメモリ内の物理的に) どこに、何があるのでしょうか?
  • OS または言語ランタイムによってどの程度制御されますか?
  • その範囲は何ですか?
  • それぞれのサイズは何によって決まるのでしょうか?
  • 何が速くなるのでしょうか?
役に立ちましたか?

解決

スタックは、実行スレッドのスクラッチ領域として確保されるメモリです。関数が呼び出されると、ローカル変数と一部の簿記データ用にスタックの最上部にブロックが予約されます。その関数が返されると、ブロックは未使用になり、次回関数が呼び出されたときに使用できるようになります。スタックは常に LIFO (後入れ先出し) 順序で予約されます。次に解放されるブロックは常に、最後に予約されたブロックです。これにより、スタックを追跡するのが非常に簡単になります。スタックからブロックを解放することは、1 つのポインターを調整することに他なりません。

ヒープは、動的割り当てのために確保されているメモリです。スタックとは異なり、ヒープからのブロックの割り当てと割り当て解除に強制的なパターンはありません。いつでもブロックを割り当て、いつでも解放できます。このため、ヒープのどの部分が割り当てられているか、または解放されているかを常に追跡することが非常に複雑になります。さまざまな使用パターンに合わせてヒープのパフォーマンスを調整するために使用できるカスタム ヒープ アロケータが多数あります。

各スレッドはスタックを取得しますが、通常、アプリケーションのヒープは 1 つだけです (ただし、さまざまな種類の割り当てに対して複数のヒープがあることは珍しくありません)。

質問に直接答えるには:

OS または言語ランタイムによってどの程度制御されますか?

OS は、スレッドの作成時にシステムレベルのスレッドごとにスタックを割り当てます。通常、OS は言語ランタイムによって呼び出され、アプリケーションにヒープを割り当てます。

その範囲は何ですか?

スタックはスレッドに接続されているため、スレッドが終了するとスタックは再利用されます。ヒープは通常、アプリケーションの起動時にランタイムによって割り当てられ、アプリケーション (技術的にプロセス) が終了すると再利用されます。

それぞれのサイズは何によって決まるのでしょうか?

スタックのサイズは、スレッドの作成時に設定されます。ヒープのサイズはアプリケーションの起動時に設定されますが、領域が必要になると増加する可能性があります (アロケーターはオペレーティング システムにより多くのメモリを要求します)。

何が速くなるのでしょうか?

スタックはアクセス パターンによりメモリの割り当てと割り当て解除が簡単になる (ポインタ/整数は単純にインクリメントまたはデクリメントされる) ため、スタックが高速になります。一方、ヒープでは割り当てまたは割り当て解除に関連するはるかに複雑なブックキーピングが行われます。また、スタック内の各バイトは非常に頻繁に再利用される傾向があるため、プロセッサのキャッシュにマップされる傾向があり、非常に高速になります。ヒープのもう 1 つのパフォーマンス ヒットは、ヒープが主にグローバル リソースであるため、通常はマルチスレッドで安全である必要があることです。それぞれの割り当てと割り当て解除は、通常、プログラム内の他の「すべての」ヒープ アクセスと同期する必要があります。

明確なデモンストレーション:
画像出典: vikashazrati.wordpress.com

他のヒント

スタック:

  • ヒープと同様にコンピュータの RAM に保存されます。
  • スタック上に作成された変数はスコープ外になり、自動的に割り当てが解除されます。
  • ヒープ上の変数と比較して、はるかに高速に割り当てることができます。
  • 実際のスタック データ構造で実装されます。
  • パラメータの受け渡しに使用されるローカル データ、リターン アドレスを格納します。
  • スタックが多すぎると、スタック オーバーフローが発生する可能性があります (主に、無限または深すぎる再帰、非常に大規模な割り当てが原因)。
  • スタック上に作成されたデータはポインタなしで使用できます。
  • コンパイル前に割り当てる必要があるデータ量が正確にわかっていて、データ量が大きすぎない場合は、スタックを使用します。
  • 通常、最大サイズはプログラムの開始時にすでに決定されています。

ヒープ:

  • スタックと同様にコンピュータの RAM に保存されます。
  • C++ では、ヒープ上の変数は手動で破棄する必要があり、決してスコープ外にならないようにしてください。データは次のように解放されます delete, delete[], 、 または free.
  • スタック上の変数と比較して割り当てが遅くなります。
  • プログラムで使用するデータのブロックを割り当てるためにオンデマンドで使用されます。
  • 大量の割り当てと割り当て解除が行われると、断片化が発生する可能性があります。
  • C++ または C では、ヒープ上に作成されたデータはポインターによってポイントされ、次のように割り当てられます。 new または malloc それぞれ。
  • 大きすぎるバッファの割り当てが要求された場合、割り当てエラーが発生する可能性があります。
  • 実行時に必要なデータ量が正確にわからない場合、または大量のデータを割り当てる必要がある場合は、ヒープを使用します。
  • メモリリークの原因となります。

例:

int foo()
{
  char *pBuffer; //<--nothing allocated yet (excluding the pointer itself, which is allocated here on the stack).
  bool b = true; // Allocated on the stack.
  if(b)
  {
    //Create 500 bytes on the stack
    char buffer[500];

    //Create 500 bytes on the heap
    pBuffer = new char[500];

   }//<-- buffer is deallocated here, pBuffer is not
}//<--- oops there's a memory leak, I should have called delete[] pBuffer;

最も重要な点は、ヒープとスタックはメモリを割り当てる方法の総称であるということです。これらはさまざまな方法で実装でき、用語は基本概念に適用されます。

  • アイテムのスタックでは、アイテムはそこに配置された順序で他のアイテムの上に重なり、(全体をひっくり返さずに) 一番上のアイテムのみを削除できます。

    Stack like a stack of papers

    スタックの単純さは、割り当てられたメモリの各セクションのレコードを含むテーブルを維持する必要がないことです。必要な状態情報は、スタックの末尾への単一のポインタだけです。割り当てと割り当て解除を行うには、その 1 つのポインターをインクリメントおよびデクリメントするだけです。注記:スタックは、メモリのセクションの先頭から開始して、上向きに成長するのではなく、下向きに拡張するように実装される場合があります。

  • ヒープ内では、アイテムの配置方法に特別な順序はありません。明確な「一番上」のアイテムがないため、任意の順序でアイテムにアクセスしたり削除したりできます。

    Heap like a heap of licorice allsorts

    ヒープ割り当てには、どのメモリが割り当てられ、何が割り当てられていないのかの完全な記録を維持する必要があるほか、断片化を軽減したり、要求されたサイズに適合する十分な大きさの連続したメモリ セグメントを見つけたりするためのオーバーヘッド メンテナンスも必要です。メモリはいつでも割り当てを解除して空き領域を残すことができます。場合によっては、メモリ アロケータは、割り当てられたメモリを移動することによるメモリのデフラグや、実行時にメモリがスコープ内になくなったことを特定して割り当てを解除するガベージ コレクションなどのメンテナンス タスクを実行することがあります。

これらのイメージは、スタックとヒープでメモリを割り当ておよび解放する 2 つの方法をかなりうまく説明します。うーん!

  • OS または言語ランタイムによってどの程度制御されますか?

    前述したように、ヒープとスタックは一般的な用語であり、さまざまな方法で実装できます。通常、コンピュータ プログラムには、 コールスタック これには、呼び出し元の関数へのポインタやローカル変数など、現在の関数に関連する情報が格納されます。関数は他の関数を呼び出してから戻るため、呼び出しスタックの下位にある関数からの情報を保持するためにスタックが拡大または縮小します。プログラムは実際には実行時に制御できません。それはプログラミング言語、OS、さらにはシステム アーキテクチャによっても決まります。

    ヒープは、動的かつランダムに割り当てられるメモリを指す一般用語です。つまり故障中。通常、メモリは OS によって割り当てられ、アプリケーションは API 関数を呼び出してこの割り当てを行います。動的に割り当てられたメモリの管理にはかなりのオーバーヘッドが必要で、通常は OS によって処理されます。

  • その範囲は何ですか?

    コールスタックは非常に低レベルの概念であるため、プログラミングの意味での「スコープ」とは関係ありません。一部のコードを逆アセンブルすると、スタックの一部への相対ポインター スタイルの参照が表示されますが、高水準言語に関する限り、言語は独自のスコープ ルールを課します。ただし、スタックの重要な側面の 1 つは、関数が返されると、その関数にローカルなものはすべてスタックから直ちに解放されるということです。これは、プログラミング言語の動作を考慮すると、期待どおりに機能します。ヒープでは、定義するのも困難です。スコープは OS によって公開されるものですが、おそらく、アプリケーション内の「スコープ」が何であるかについてのルールをプログラミング言語が追加します。プロセッサ アーキテクチャと OS は仮想アドレス指定を使用しており、プロセッサが物理アドレスに変換するため、ページ フォールトなどが発生します。どのページがどのアプリケーションに属しているかを追跡します。ただし、プログラミング言語がメモリの割り当てと解放に使用するあらゆる方法を使用し、エラー (何らかの理由で割り当て/解放が失敗した場合) をチェックするだけなので、これについて実際に心配する必要はありません。

  • それぞれのサイズは何によって決まるのでしょうか?

    繰り返しになりますが、言語、コンパイラ、オペレーティング システム、アーキテクチャによって異なります。定義上、スタックは連続したメモリでなければならないため、通常、スタックは事前に割り当てられます (詳細は最後の段落で説明します)。そのサイズは言語コンパイラまたは OS によって決まります。巨大なデータの塊をスタックに保存するわけではないため、望ましくない無限再帰 (したがって「スタック オーバーフロー」) やその他の異常なプログラミング上の決定が発生する場合を除いて、十分に大きいデータを完全に使用することはできません。

    ヒープとは、動的に割り当てることができるものの総称です。見る方向によって、大きさは常に変化します。最新のプロセッサとオペレーティング システムでは、その正確な動作方法はいずれにせよ非常に抽象化されているため、(それが可能な言語では) メモリを使用してはいけないことを除いて、通常、それがどのように動作するかについて深く心配する必要はありません。まだ割り当てていないか、解放したメモリ。

  • 何が速くなるのでしょうか?

    すべての空きメモリが常に連続しているため、スタックは高速になります。空きメモリのすべてのセグメントのリストを保持する必要はなく、スタックの現在のトップへの単一のポインタだけを保持する必要があります。コンパイラは通常、このポインタを特別な高速ファイルに保存します。 登録する この目的のために。さらに、スタック上での後続の操作は、通常、メモリの非常に近い領域内に集中します。これは、非常に低いレベルでは、プロセッサのオンダイ キャッシュによる最適化に適しています。

(この回答は、多かれ少なかれこの質問のカモだった別の質問から移動しました。)

あなたの質問に対する答えは実装によって異なり、コンパイラやプロセッサ アーキテクチャによって異なる場合があります。ただし、ここでは簡略化して説明します。

  • スタックとヒープは両方とも、基礎となるオペレーティング システムから割り当てられたメモリ領域です (多くの場合、オンデマンドで物理メモリにマップされる仮想メモリ)。
  • マルチスレッド環境では、各スレッドは完全に独立した独自のスタックを持ちますが、ヒープを共有します。同時アクセスはヒープ上で制御する必要があり、スタック上では制御できません。

ヒープ

  • ヒープには、使用済みブロックと空きブロックのリンクされたリストが含まれています。ヒープ上の新しい割り当て ( new または malloc)は、空きブロックの 1 つから適切なブロックを作成することで満たされます。これには、ヒープ上のブロックのリストを更新する必要があります。これ メタ情報 ヒープ上のブロックに関する情報も、多くの場合、ヒープ上の各ブロックの直前の小さな領域に格納されます。
  • ヒープが増大すると、多くの場合、新しいブロックが下位アドレスから上位アドレスに向かって割り当てられます。したがって、ヒープを次のように考えることができます。 ヒープ メモリが割り当てられるにつれてサイズが増加するメモリ ブロックのサイズ。ヒープが割り当てに対して小さすぎる場合は、多くの場合、基盤となるオペレーティング システムからより多くのメモリを取得することでサイズを増やすことができます。
  • 多数の小さなブロックの割り当てと割り当て解除を行うと、使用済みブロックの間に多数の小さな空きブロックが散在する状態でヒープが残る可能性があります。空きブロックの合計サイズが十分に大きい場合でも、空きブロックのどれも割り当て要求を満たすのに十分な大きさがないため、大きなブロックを割り当てる要求は失敗することがあります。これはと呼ばれます ヒープの断片化.
  • フリー ブロックに隣接する使用済みブロックの割り当てが解除されると、新しいフリー ブロックが隣接するフリー ブロックとマージされて、より大きなフリー ブロックが作成され、ヒープの断片化が効果的に削減されます。

The heap

スタック

  • スタックは多くの場合、CPU 上の特殊レジスターと密接に連携して動作します。 スタックポインタ. 。最初、スタック ポインタはスタックの先頭 (スタック上の最上位アドレス) を指します。
  • CPUには特別な命令があります。 押す 値をスタックに置き、 はじける それらをスタックから戻します。それぞれ 押す スタック ポインタの現在の位置に値を格納し、スタック ポインタを減少させます。あ ポップ スタック ポインタが指す値を取得し、スタック ポインタを増やします (という事実に混乱しないでください)。 追加する スタックへの値 減少する スタックポインタと 削除する 価値 増加する それ。スタックは一番下まで成長することに注意してください)。保存および取得される値は、CPU レジスタの値です。
  • 関数が呼び出されるとき、CPU は現在のデータをプッシュする特別な命令を使用します。 命令ポインタ, 、つまりスタック上で実行されているコードのアドレス。次に、CPUは、呼び出された関数のアドレスに命令ポインターを設定することにより、関数にジャンプします。その後、関数が戻ると、古い命令ポインターがスタックからポップされ、関数呼び出し直後のコードから実行が再開されます。
  • 関数が入力されると、スタック ポインタが減らされて、ローカル (自動) 変数用にスタック上により多くのスペースが割り当てられます。関数に 1 つのローカル 32 ビット変数がある場合、4 バイトがスタック上に確保されます。関数が戻ると、スタック ポインタが戻されて、割り当てられた領域が解放されます。
  • 関数にパラメータがある場合、これらは関数の呼び出し前にスタックにプッシュされます。関数内のコードは、現在のスタック ポインターからスタックを上に移動して、これらの値を見つけることができます。
  • ネストされた関数呼び出しは魅力的に機能します。新しい呼び出しごとに、関数パラメータ、戻りアドレス、ローカル変数のスペースが割り当てられます。 アクティベーションレコード ネストされた呼び出しに対してスタックすることができ、関数が返されるときに正しい方法でアンワインドされます。
  • スタックは限られたメモリ ブロックであるため、次のような問題が発生する可能性があります。 スタックオーバーフロー ネストされた関数の呼び出しが多すぎたり、ローカル変数に割り当てられたスペースが多すぎたりすることによって発生します。多くの場合、スタックに使用されるメモリ領域は、スタックの最下位 (最下位アドレス) より下に書き込むと CPU でトラップまたは例外がトリガーされるように設定されています。この例外的な状態はランタイムによって捕捉され、ある種のスタック オーバーフロー例外に変換されます。

The stack

関数をスタックではなくヒープに割り当てることはできますか?

いいえ、機能のアクティベーション レコード (つまり、ローカル変数または自動変数) は、これらの変数を格納するためだけでなく、ネストされた関数呼び出しを追跡するためにも使用されるスタックに割り当てられます。

ヒープがどのように管理されるかは、実際にはランタイム環境によって決まります。C は使用します malloc C++ では new, しかし他の多くの言語にはガベージコレクションがあります。

ただし、スタックはプロセッサ アーキテクチャに密接に関係する、より低レベルの機能です。十分なスペースがない場合にヒープを拡張することは、ヒープを処理するライブラリ呼び出しで実装できるため、それほど難しくありません。ただし、スタックのオーバーフローは手遅れになって初めて発見されるため、スタックの拡張は不可能なことがよくあります。そして、実行スレッドをシャットダウンすることが唯一の実行可能な選択肢です。

次の C# コードでは

public void Method1()
{
    int i = 4;
    int y = 2;
    class1 cls1 = new class1();
}

メモリは次のように管理されます

Picture of variables on the stack

Local Variables これは、関数呼び出しがスタック内にある間だけ存続する必要があります。ヒープは、有効期間が事前に実際には分からないが、しばらく続くことが予想される変数に使用されます。ほとんどの言語では、変数をスタックに格納する場合、コンパイル時にその変数の大きさを知ることが重要です。

オブジェクト (更新するとサイズが変化します) は、作成時にどれくらい存続するかわからないため、ヒープ上に置かれます。多くの言語では、参照を持たなくなったオブジェクト (cls1 オブジェクトなど) を見つけるためにヒープがガベージ コレクションされます。

Java では、ほとんどのオブジェクトはヒープに直接置かれます。C / C++ などの言語では、ポインターを処理していないときに構造体やクラスがスタック上に残ることがよくあります。

詳細については、こちらをご覧ください。

スタックとヒープのメモリ割り当ての違い « timmurphy.org

そしてここ:

スタックとヒープ上にオブジェクトを作成する

この記事は上記の画像の出典です。 .NET の 6 つの重要な概念:スタック、ヒープ、値型、参照型、ボックス化、およびボックス化解除 - CodeProject

ただし、多少の誤りが含まれる可能性があることに注意してください。

スタック関数を呼び出すと、その関数の引数とその他のオーバーヘッドがスタックに置かれます。一部の情報 (帰国時にどこに行くかなど) もそこに保存されます。関数内で変数を宣言すると、その変数もスタック上に割り当てられます。

スタックの割り当て解除は、常に割り当てとは逆の順序で行われるため、非常に簡単です。関数を開始するとスタック要素が追加され、関数を終了すると対応するデータが削除されます。これは、他の多くの関数を呼び出す関数をたくさん呼び出さない限り (または再帰的ソリューションを作成しない限り)、スタックの小さな領域内にとどまる傾向があることを意味します。

ヒープヒープは、オンザフライで作成したデータを配置する場所の一般的な名前です。プログラムで作成する宇宙船の数がわからない場合は、new (または malloc または同等の) 演算子を使用して各宇宙船を作成する可能性があります。この割り当てはしばらく続くため、作成した順序とは異なる順序で解放することになる可能性があります。

したがって、未使用のメモリ領域がチャンクとインターリーブされることになり、メモリが断片化されるため、ヒープははるかに複雑になります。必要なサイズの空きメモリを見つけるのは難しい問題です。これが、ヒープを避けるべき理由です (ただし、ヒープは依然としてよく使用されます)。

実装スタックとヒープの実装は通常、ランタイム/OS によって決まります。多くの場合、パフォーマンスが重要なゲームやその他のアプリケーションは、メモリを OS に依存することを避けるために、ヒープから大量のメモリを取得し、それを内部で分配する独自のメモリ ソリューションを作成します。

これは、メモリ使用量が標準と大きく異なる場合にのみ実用的です。つまり、1 つの巨大な操作でレベルをロードし、別の巨大な操作ですべてを放り出すことができるゲームの場合です。

メモリ内の物理的な場所と呼ばれるテクノロジーのせいで、これはあなたが思っているよりも重要ではありません。 仮想メモリ これにより、プログラムは、物理データが別の場所 (ハードディスク上であっても!) にある特定のアドレスにアクセスできると認識します。スタックに対して取得するアドレスは、コール ツリーが深くなるにつれて昇順になります。ヒープのアドレスは予測不可能 (つまり、実装固有) であり、率直に言って重要ではありません。

明確にするために、 この答え 間違った情報があります (トーマス コメントの後に彼の答えを修正しました、クールです:))。他の回答では、静的割り当ての意味の説明を避けています。そこで、以下では 3 つの主要な割り当て形式と、それらが通常どのようにヒープ、スタック、データ セグメントに関連するかを説明します。また、理解を助けるために、C/C++ と Python の両方で例をいくつか示します。

「静的」(別名静的に割り当てられる) 変数はスタックに割り当てられません。そう思い込まないでください。多くの人は、「静的」という言葉が「スタック」によく似ているという理由だけでそう思います。これらは実際にはスタックにもヒープにも存在しません。と呼ばれるものの一部です。 データセグメント.

ただし、一般的には「」を考慮した方がよいでしょう。範囲" そして "一生「スタック」や「ヒープ」ではなく。

スコープとは、コードのどの部分が変数にアクセスできるかを指します。一般的に私たちが考えるのは、 ローカルスコープ (現在の関数によってのみアクセス可能) と比較 グローバルスコープ (どこからでもアクセス可能) ただし、スコープはさらに複雑になる可能性があります。

ライフタイムとは、プログラムの実行中に変数が割り当てられ、割り当てが解除される時間を指します。通常私たちが考えるのは 静的割り当て (変数はプログラムの継続時間全体にわたって持続するため、複数の関数呼び出しにわたって同じ情報を保存するのに役立ちます) と比較 自動割り当て (変数は関数の 1 回の呼び出し中にのみ保持されるため、関数の実行中にのみ使用され、関数の終了後に破棄できる情報を格納するのに役立ちます) と比較 動的割り当て (静的または自動のようなコンパイル時ではなく、実行時に期間が定義される変数)。

ほとんどのコンパイラとインタプリタは、スタックやヒープなどの使用に関して同様にこの動作を実装しますが、動作が正しい限り、コンパイラは必要に応じてこれらの規則を破ることもあります。たとえば、最適化により、ほとんどのローカル変数がスタック内に存在する場合でも、ローカル変数はレジスタ内にのみ存在するか、完全に削除される場合があります。いくつかのコメントで指摘されているように、スタックやヒープを使用せず、代わりに他のストレージ メカニズムを使用するコンパイラーを自由に実装できます (スタックとヒープはこれに最適であるため、めったに実行されません)。

このすべてを説明するために、注釈付きの簡単な C コードをいくつか紹介します。学習するための最良の方法は、デバッガーでプログラムを実行し、その動作を観察することです。Python を読みたい場合は、回答の最後までスキップしてください:)

// Statically allocated in the data segment when the program/DLL is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in the code
int someGlobalVariable;

// Statically allocated in the data segment when the program is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in this particular code file
static int someStaticVariable;

// "someArgument" is allocated on the stack each time MyFunction is called
// "someArgument" is deallocated when MyFunction returns
// scope - can be accessed only within MyFunction()
void MyFunction(int someArgument) {

    // Statically allocated in the data segment when the program is first loaded
    // Deallocated when the program/DLL exits
    // scope - can be accessed only within MyFunction()
    static int someLocalStaticVariable;

    // Allocated on the stack each time MyFunction is called
    // Deallocated when MyFunction returns
    // scope - can be accessed only within MyFunction()
    int someLocalVariable;

    // A *pointer* is allocated on the stack each time MyFunction is called
    // This pointer is deallocated when MyFunction returns
    // scope - the pointer can be accessed only within MyFunction()
    int* someDynamicVariable;

    // This line causes space for an integer to be allocated in the heap
    // when this line is executed. Note this is not at the beginning of
    // the call to MyFunction(), like the automatic variables
    // scope - only code within MyFunction() can access this space
    // *through this particular variable*.
    // However, if you pass the address somewhere else, that code
    // can access it too
    someDynamicVariable = new int;


    // This line deallocates the space for the integer in the heap.
    // If we did not write it, the memory would be "leaked".
    // Note a fundamental difference between the stack and heap
    // the heap must be managed. The stack is managed for us.
    delete someDynamicVariable;

    // In other cases, instead of deallocating this heap space you
    // might store the address somewhere more permanent to use later.
    // Some languages even take care of deallocation for you... but
    // always it needs to be taken care of at runtime by some mechanism.

    // When the function returns, someArgument, someLocalVariable
    // and the pointer someDynamicVariable are deallocated.
    // The space pointed to by someDynamicVariable was already
    // deallocated prior to returning.
    return;
}

// Note that someGlobalVariable, someStaticVariable and
// someLocalStaticVariable continue to exist, and are not
// deallocated until the program exits.

ライフタイムとスコープを区別することが重要である理由の特に痛ましい例は、変数にはローカル スコープを持つことができますが、ライフタイムは静的であることが挙げられます。たとえば、上記のコード サンプルの「someLocalStaticVariable」です。このような変数により、私たちの一般的だが非公式な命名習慣が非常に混乱する可能性があります。たとえば、「」と言うとき、地元「私たちは通常、こう言います」ローカルスコープの自動割り当て変数「そして私たちがグローバルと言うとき、通常は「」を意味しますグローバルスコープの静的に割り当てられた変数」。残念ながら「」のようなことになると、ファイルスコープの静的に割り当てられた変数「多くの人はただこう言います...」はぁ???".

C/C++ の構文の選択によっては、この問題がさらに悪化します。たとえば、以下に示す構文のせいで、多くの人がグローバル変数が「静的」ではないと考えています。

int var1; // Has global scope and static allocation
static int var2; // Has file scope and static allocation

int main() {return 0;}

上記の宣言にキーワード「static」を入れると、var2 がグローバル スコープを持つことができなくなることに注意してください。それにもかかわらず、グローバル var1 には静的割り当てがあります。これは直感的ではありません。このため、私はスコープを説明するときに「静的」という言葉を決して使わず、代わりに「ファイル」または「ファイル限定」スコープのような言い方をするようにしています。ただし、多くの人は、1 つのコード ファイルからのみアクセスできる変数を説明するために「静的」または「静的スコープ」というフレーズを使用します。生涯という文脈では「静的」 いつも 変数はプログラムの開始時に割り当てられ、プログラムの終了時に割り当てが解除されることを意味します。

これらの概念を C/C++ 固有のものと考える人もいます。ではない。たとえば、以下の Python サンプルは、3 種類の割り当てすべてを示しています (解釈言語には微妙な違いがいくつかありますが、ここでは説明しません)。

from datetime import datetime

class Animal:
    _FavoriteFood = 'Undefined' # _FavoriteFood is statically allocated

    def PetAnimal(self):
        curTime = datetime.time(datetime.now()) # curTime is automatically allocatedion
        print("Thank you for petting me. But it's " + str(curTime) + ", you should feed me. My favorite food is " + self._FavoriteFood)

class Cat(Animal):
    _FavoriteFood = 'tuna' # Note since we override, Cat class has its own statically allocated _FavoriteFood variable, different from Animal's

class Dog(Animal):
    _FavoriteFood = 'steak' # Likewise, the Dog class gets its own static variable. Important to note - this one static variable is shared among all instances of Dog, hence it is not dynamic!


if __name__ == "__main__":
    whiskers = Cat() # Dynamically allocated
    fido = Dog() # Dynamically allocated
    rinTinTin = Dog() # Dynamically allocated

    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

    Dog._FavoriteFood = 'milkbones'
    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

# Output is:
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is milkbones
# Thank you for petting me. But it's 13:05:02.256000, you should feed me. My favorite food is milkbones

他の人は大まかな質問に非常にうまく答えているので、いくつかの詳細を追加します。

  1. スタックとヒープは単数である必要はありません。複数のスタックがある一般的な状況は、プロセス内に複数のスレッドがある場合です。この場合、各スレッドには独自のスタックがあります。複数のヒープを持つこともできます。たとえば、DLL 構成によっては、異なる DLL が異なるヒープから割り当てられる可能性があります。そのため、別のライブラリによって割り当てられたメモリを解放するのは一般的に悪い考えです。

  2. C では、次の使用を通じて可変長割り当ての利点を得ることができます。 アロカ, ヒープ上に割り当てる alloc とは対照的に、スタック上に割り当てます。このメモリは return ステートメントの後は残りませんが、スクラッチ バッファとしては役立ちます。

  3. あまり使用しない Windows 上に巨大な一時バッファを作成するのは無料ではありません。これは、関数が入力されるたびにスタックが存在することを確認するためにコンパイラが呼び出されるスタック プローブ ループを生成するためです (Windows はスタックの拡張が必要な​​時期を検出するためにスタックの最後に単一のガード ページを使用するためです)。スタックの最後から 1 ページ以上離れたメモリにアクセスするとクラッシュします)。例:

void myfunction()
{
   char big[10000000];
   // Do something that only uses for first 1K of big 99% of the time.
}

他の人があなたの質問に直接答えていますが、スタックとヒープを理解しようとするときは、従来のUNIXプロセス(スレッドと mmap()-ベースのアロケータ)。の メモリ管理用語集 Web ページには、このメモリ レイアウトの図が掲載されています。

スタックとヒープは伝統的に、プロセスの仮想アドレス空間の反対側に配置されます。スタックは、アクセスされると、カーネルによって設定されたサイズまで自動的に増加します (サイズは次のように調整できます)。 setrlimit(RLIMIT_STACK, ...))。メモリ アロケータが brk() または sbrk() システム コールを使用して、より多くの物理メモリ ページをプロセスの仮想アドレス空間にマッピングします。

一部の組み込みシステムなど、仮想メモリのないシステムでは、スタックとヒープのサイズが固定されていることを除いて、同じ基本レイアウトが適用されることがよくあります。ただし、他の組み込みシステム (Microchip PIC マイクロコントローラに基づくシステムなど) では、プログラム スタックはデータ移動命令ではアドレス指定できない別個のメモリ ブロックであり、プログラム フロー命令 (呼び出し、返品など)。Intel Itanium プロセッサなどの他のアーキテクチャには、 複数のスタック. 。この意味で、スタックは CPU アーキテクチャの要素です。

スタックはメモリの一部であり、「ポップ」(スタックから値を削除して返す)や「プッシュ」(スタックに値をプッシュする)などのいくつかの主要なアセンブリ言語命令を介して操作できるほか、(サブルーチンを呼び出します - これはアドレスをプッシュしてスタックに返します)そして戻ります(サブルーチンから戻ります - これはアドレスをスタックからポップしてそこにジャンプします)。これはスタック ポインタ レジスタの下のメモリ領域であり、必要に応じて設定できます。スタックは、サブルーチンに引数を渡す場合や、サブルーチンを呼び出す前にレジスタ内の値を保存する場合にも使用されます。

ヒープは、オペレーティング システムによって、通常は malloc などのシステムコールを通じてアプリケーションに与えられるメモリの一部です。最新の OS では、このメモリは呼び出しプロセスのみがアクセスできるページのセットです。

スタックのサイズは実行時に決定され、通常はプログラムの起動後には増加しません。C プログラムでは、スタックは、各関数内で宣言されたすべての変数を保持するのに十分な大きさである必要があります。ヒープは必要に応じて動的に拡大しますが、最終的に呼び出しを行うのは OS です (多くの場合、malloc によって要求された値を超えてヒープが拡大されるため、少なくとも一部の将来の malloc はカーネルに戻る必要がなくなります)より多くのメモリを取得します。多くの場合、この動作はカスタマイズ可能です)

プログラムを起動する前にスタックを割り当てているため、スタックを使用する前に malloc する必要がないため、これにはわずかな利点があります。実際には、仮想メモリ サブシステムを備えた最新のオペレーティング システムで何が高速になり、何が低速になるかを予測することは非常に困難です。これは、ページがどのように実装され、どこに保存されるかが実装の詳細であるためです。

この件に関しては、他の多くの人がほぼ正しい答えを出してくれていると思います。

ただし、見逃されている詳細の 1 つは、「ヒープ」は実際にはおそらく「フリー ストア」と呼ばれるべきだということです。この区別の理由は、元のフリーストアが「二項山」として知られるデータ構造で実装されたためです。そのため、malloc()/free()の早期実装からの割り当ては、ヒープからの割り当てでした。ただし、現代では、ほとんどの無料ストアは二項ヒープではない非常に精巧なデータ構造で実装されています。

スタックとは何ですか?

スタックとはオブジェクトの山であり、通常はきちんと配置されています。

Enter image description here

コンピューティング アーキテクチャにおけるスタックは、データが後入れ先出し方式で追加または削除されるメモリ領域です。
マルチスレッド アプリケーションでは、各スレッドが独自のスタックを持ちます。

ヒープとは何ですか?

ヒープとは、無造作に積み上げられたものを乱雑に集めたものです。

Enter image description here

コンピューティング アーキテクチャにおけるヒープは、オペレーティング システムまたはメモリ マネージャー ライブラリによって自動的に管理される、動的に割り当てられるメモリの領域です。
ヒープ上のメモリはプログラムの実行中に定期的に割り当て、割り当て解除、サイズ変更が行われるため、断片化と呼ばれる問題が発生する可能性があります。
断片化は、追加のメモリ オブジェクトを保持するには小さすぎる小さなスペースを間に挟んでメモリ オブジェクトが割り当てられる場合に発生します。
最終的な結果は、それ以降のメモリ割り当てに使用できないヒープ領域の割合になります。

両方一緒に

マルチスレッド アプリケーションでは、各スレッドが独自のスタックを持ちます。ただし、さまざまなスレッドはすべてヒープを共有します。
マルチスレッド アプリケーションではさまざまなスレッドがヒープを共有するため、スレッドがヒープ内の同じメモリ部分にアクセスして操作しようとしないように、スレッド間で何らかの調整が必要になることも意味します。同じ時間です。

スタックとヒープのどちらが速いでしょうか?なぜ?

スタックはヒープよりもはるかに高速です。
これは、スタック上でのメモリの割り当て方法が原因です。
スタック上にメモリを割り当てるのは、スタック ポインタを上に移動するだけです。

プログラミングに慣れていない人にとっては、簡単なスタックを使用することをお勧めします。
スタックは小さいため、データに必要なメモリ量が正確にわかっている場合、またはデータのサイズが非常に小さいことがわかっている場合に使用するとよいでしょう。
データに大量のメモリが必要になることがわかっている場合、または必要なメモリ量がわからない場合 (動的配列の場合など)、ヒープを使用することをお勧めします。

Javaメモリモデル

Enter image description here

スタックは、ローカル変数 (メソッド パラメーターを含む) が保存されるメモリ領域です。オブジェクト変数に関しては、ヒープ上の実際のオブジェクトへの単なる参照 (ポインター) にすぎません。
オブジェクトがインスタンス化されるたびに、そのオブジェクトのデータ (状態) を保持するためにヒープ メモリのチャンクが確保されます。オブジェクトには他のオブジェクトを含めることができるため、実際には、このデータの一部はそれらのネストされたオブジェクトへの参照を保持できます。

スタックを使用すると、いくつかの興味深いことができます。たとえば、次のような関数があります アロカ (使用に関する大量の警告を回避できると仮定します)。これは、特にメモリにヒープではなくスタックを使用する malloc の形式です。

とはいえ、スタックベースのメモリ エラーは、私が経験した中でも最悪のエラーの 1 つです。ヒープ メモリを使用していて、割り当てられたブロックの境界を超えると、セグメント フォールトが発生する可能性が高くなります。(100%ではありません:ただし、スタック上に作成された変数は常に相互に連続しているため、範囲外に書き込むと別の変数の値が変更される可能性があります。自分のプログラムが論理の法則に従わなくなったと感じたときは、それはおそらくバッファ オーバーフローであることがわかりました。

簡単に言えば、スタックはローカル変数が作成される場所です。また、サブルーチンを呼び出すたびに、プログラム カウンター (次の機械語命令へのポインター) と重要なレジスタ、および場合によってはパラメーターがスタックにプッシュされます。次に、サブルーチン内のローカル変数がスタックにプッシュされます (そしてそこから使用されます)。サブルーチンが終了すると、その内容はすべてスタックからポップされて戻されます。PC とレジスタのデータはポップされたときに取得され、元の場所に戻されるため、プログラムは順調に進むことができます。

ヒープは、動的メモリ割り当て (明示的な "new" または "allocate" 呼び出し) が行われるメモリの領域です。これは、さまざまなサイズのメモリ ブロックとその割り当てステータスを追跡できる特別なデータ構造です。

「クラシック」システムでは、スタック ポインタがメモリの下部から始まり、ヒープ ポインタが上部から始まり、それらが互いに増加するように RAM がレイアウトされていました。それらが重なっている場合は、RAM が不足しています。ただし、これは最新のマルチスレッド OS では機能しません。すべてのスレッドには独自のスタックが必要であり、それらは動的に作成できます。

ウィキアンサーより。

スタック

関数またはメソッドが別の関数を呼び出し、その関数がさらに別の関数を呼び出す場合など、最後の関数が値を返すまで、それらすべての関数の実行は中断されたままになります。

スタック内の要素 (関数呼び出し) は相互に依存しているため、この中断された関数呼び出しのチェーンがスタックとなります。

スタックは、例外処理とスレッドの実行において考慮することが重要です。

ヒープ

ヒープは、プログラムが変数を保存するために使用するメモリです。ヒープの要素 (変数) は相互に依存関係がなく、いつでもランダムにアクセスできます。

スタック

  • 非常に高速なアクセス
  • 明示的に変数の割り当てを解除する必要はありません
  • スペースはCPUによって効率的に管理され、メモリは断片化されません。
  • ローカル変数のみ
  • スタック サイズの制限 (OS に依存)
  • 変数のサイズは変更できません

ヒープ

  • 変数にはグローバルにアクセスできます
  • メモリサイズに制限なし
  • (比較的) アクセスが遅い
  • スペースの効率的な使用は保証されておらず、メモリのブロックが割り当てられてから解放されるため、時間の経過とともにメモリが断片化する可能性があります。
  • メモリを管理する必要があります (変数の割り当てと解放はあなたが担当します)
  • realloc()を使用して変数のサイズを変更できます

わかりました、 簡単にそして短い言葉で言うと、 順序付けられました そして 注文されていない...!

スタック:スタック項目では、物がお互いの上に重なります。これは、処理がより速く、より効率的になることを意味します。

したがって、特定の項目を指すインデックスが常に存在し、処理も高速化され、項目間にも関連性があります。

ヒープ:順序がないと処理が遅くなり、特定の順序やインデックスがないため値がめちゃくちゃになります...ランダムであり、それらの間に関係はありません...そのため、実行時間と使用時間は異なる可能性があります...

また、どのように見えるかを示すために、以下の画像も作成します。

enter image description here

要するに

スタックは静的メモリ割り当てに使用され、ヒープは動的メモリ割り当てに使用され、どちらもコンピュータの RAM に保存されます。


詳細に

スタック

スタックは「LIFO」(後入れ先出し) データ構造であり、CPU によって非常に厳密に管理および最適化されます。関数が新しい変数を宣言するたびに、その変数はスタックに「プッシュ」されます。その後、関数が終了するたびに、その関数によってスタックにプッシュされたすべての変数が解放されます (つまり、削除されます)。スタック変数が解放されると、そのメモリ領域は他のスタック変数で使用できるようになります。

スタックを使用して変数を保存する利点は、メモリが自動的に管理されることです。手動でメモリを割り当てたり、不要になったらメモリを解放したりする必要はありません。さらに、CPU はスタック メモリを非常に効率的に編成するため、スタック変数の読み取りと書き込みが非常に高速になります。

さらに多くのものが見つかります ここ.


ヒープ

ヒープは、自動的には管理されず、CPU によってもそれほど厳密に管理されない、コンピューターのメモリの領域です。これは、メモリのより自由な浮動領域です (そしてより大きい)。ヒープにメモリを割り当てるには、組み込みの C 関数である malloc() または calloc() を使用する必要があります。ヒープ上にメモリを割り当てたら、必要がなくなったら、free() を使用してそのメモリの割り当てを解除する必要があります。

これを行わないと、プログラムでいわゆるメモリ リークが発生します。つまり、ヒープ上のメモリは確保されたままになります (他のプロセスでは使用できなくなります)。デバッグセクションで説明するように、と呼ばれるツールがあります。 ヴァルグリンド これはメモリ リークの検出に役立ちます。

スタックとは異なり、ヒープには可変サイズに関するサイズ制限がありません (コンピューターの明らかな物理的制限は別として)。ヒープ メモリは、ポインタを使用してヒープ上のメモリにアクセスする必要があるため、読み取りと書き込みが若干遅くなります。ポインターについては後ほど説明します。

スタックとは異なり、ヒープ上に作成された変数には、プログラム内のどこにいても、どの関数からもアクセスできます。ヒープ変数は基本的にグローバルなスコープです。

さらに多くのものが見つかります ここ.


スタック上に割り当てられた変数はメモリに直接格納され、このメモリへのアクセスは非常に高速であり、その割り当てはプログラムのコンパイル時に処理されます。関数またはメソッドが別の関数を呼び出し、その関数がさらに別の関数を呼び出す場合など、最後の関数が値を返すまで、それらすべての関数の実行は中断されたままになります。スタックは常に LIFO 順序で予約され、最後に予約されたブロックが常に次に解放されるブロックになります。これにより、スタックの追跡が非常に簡単になり、スタックからブロックを解放するのは 1 つのポインターを調整するだけです。

ヒープ上に割り当てられた変数には実行時にメモリが割り当てられ、このメモリへのアクセスは少し遅くなりますが、ヒープ サイズは仮想メモリのサイズによってのみ制限されます。ヒープの要素には相互に依存関係がなく、いつでもランダムにアクセスできます。いつでもブロックを割り当て、いつでも解放できます。このため、ヒープのどの部分が割り当てられているか、または解放されているかを常に追跡することが非常に複雑になります。

Enter image description here

コンパイル前に割り当てる必要のあるデータ量が正確にわかっていて、サイズが大きすぎない場合は、スタックを使用できます。実行時に必要なデータ量が正確にわからない場合、または大量のデータを割り当てる必要がある場合は、ヒープを使用できます。

マルチスレッドの状況では、各スレッドは完全に独立した独自のスタックを持ちますが、ヒープを共有します。スタックはスレッド固有であり、ヒープはアプリケーション固有です。スタックは、例外処理とスレッドの実行において考慮することが重要です。

各スレッドはスタックを取得しますが、通常、アプリケーションのヒープは 1 つだけです (ただし、さまざまな種類の割り当てに対して複数のヒープがあることは珍しくありません)。

Enter image description here

実行時に、アプリケーションがより多くのヒープを必要とする場合は、空きメモリからメモリを割り当てることができ、スタックがメモリを必要とする場合は、アプリケーションに割り当てられた空きメモリからメモリを割り当てることができます。

さらに詳しく説明されています ここ そして ここ.


さあ、来てください あなたの質問の答え.

OS または言語ランタイムによってどの程度制御されますか?

OS は、スレッドの作成時にシステムレベルのスレッドごとにスタックを割り当てます。通常、OS は言語ランタイムによって呼び出され、アプリケーションにヒープを割り当てます。

さらに多くのものが見つかります ここ.

その範囲は何ですか?

すでにトップで与えられています。

「コンパイル前に割り当てる必要のあるデータ量が正確にわかっていて、サイズが大きすぎない場合は、スタックを使用できます。実行時に必要なデータ量が正確にわからない場合、または大量のデータを割り当てる必要がある場合は、ヒープを使用できます。」

さらに詳しくは、 ここ.

それぞれのサイズは何によって決まるのでしょうか?

スタックのサイズは次のように設定されます。 OS スレッドが作成されたとき。ヒープのサイズはアプリケーションの起動時に設定されますが、領域が必要になると増加する可能性があります (アロケーターはオペレーティング システムにより多くのメモリを要求します)。

何が速くなるのでしょうか?

実際にはスタック ポインタを移動するだけなので、スタック割り当てははるかに高速になります。メモリ プールを使用すると、ヒープ割り当てから同等のパフォーマンスを得ることができますが、それには若干の複雑さが加わり、頭痛の種が伴います。

また、スタック vs.ヒープはパフォーマンスを考慮するだけではありません。また、オブジェクトの予想される寿命についても多くのことがわかります。

詳細は以下からご覧いただけます ここ.

1980 年代、UNIX は大企業が独自の事業を開始するにつれてうさぎのように普及しました。歴史に失われた数十のブランド名と同様に、エクソンにもそのような製品がありました。メモリがどのようにレイアウトされるかは、多くの実装者の裁量に任されていました。

典型的なCプログラムは、BRK()値を変更することで増加する機会を得て、メモリにフラットに配置されました。通常、ヒープはこのBRK値のすぐ下にあり、BRKの増加は利用可能なヒープの量を増加させました。

単一のスタックは、通常、次の固定メモリブロックの上部まで価値のないメモリの領域であったヒープの下の領域でした。この次のブロックは、多くの場合、その時代の有名なハックの1つでスタックデータによって上書きされる可能性があるコードでした。

典型的なメモリブロックの1つは、BSS(ゼロ値のブロック)で、1つのメーカーの提供で誤ってゼロにされていませんでした。もう 1 つは、文字列や数値などの初期化された値を含む DATA でした。3 番目は、CRT (C ランタイム)、メイン、関数、ライブラリを含むコードでした。

UNIX における仮想メモリの出現により、多くの制約が変わりました。これらのブロックが隣接する、またはサイズが固定されているか、特定の方法で順序付けられる必要がある理由はありません。もちろん、UNIX が登場する前には Multics が存在しており、これらの制約はありませんでした。以下は、当時のメモリ レイアウトの 1 つを示す回路図です。

A typical 1980s style UNIX C program memory layout

スタック, ヒープ そして データ 仮想メモリ内の各プロセス:

stack, heap and static data

数セント:メモリをグラフィカルに、よりシンプルに描くと良いと思います。

This is my vision of process memory construction with simplification for more easy understanding wht happening


矢印 - スタックとヒープが成長する場所、プロセス スタック サイズには OS で定義された制限があり、通常はスレッド作成 API のパラメーターによるスレッド スタック サイズ制限が表示されます。ヒープは通常、プロセスの最大仮想メモリ サイズによって制限されます(たとえば、32 ビットの場合は 2~4 GB)。

とても簡単な方法:プロセス ヒープはプロセスとその内部のすべてのスレッドに一般的で、一般的な場合に次のようなメモリ割り当てに使用されます。 malloc().

スタックは、一般的な場合の関数の戻りポインタと変数を格納し、関数呼び出しのパラメータとして処理されるローカル関数変数用の高速メモリです。

いくつかの回答はつまらないものでしたので、私は私の意見を投稿するつもりです。

驚いたことに、誰もその複数について言及していません(つまり、実行中の OS レベルのスレッドの数とは関係ありません) コール スタックは、特殊な言語 (PostScript) やプラットフォーム (Intel Itanium) だけでなく、 繊維, 緑の糸 そしていくつかの実装 コルーチン.

ファイバー、グリーン スレッド、コルーチンは多くの点で似ているため、多くの混乱が生じます。ファイバーとグリーン スレッドの違いは、前者は協調的なマルチタスクを使用するのに対し、後者は協調的またはプリエンプティブ (あるいは両方) の機能を備えていることです。ファイバーとコルーチンの区別については、を参照してください。 ここ.

いずれにせよ、ファイバー、グリーン スレッド、コルーチンの目的は、複数の関数を同時に実行することですが、 ない 並行して(参照 この質問 区別のため) 単一の OS レベルのスレッド内で、組織的な方法で制御を相互にやり取りします。

ファイバー、グリーン スレッド、またはコルーチンを使用する場合、 いつもの 関数ごとに個別のスタックがあります。(技術的には、スタックだけでなく、実行のコンテキスト全体が関数ごとにあります。最も重要なのは、CPU レジスタです。) すべてのスレッドには、同時に実行されている関数と同じ数のスタックがあり、スレッドはプログラムのロジックに従って各関数の実行を切り替えます。関数が最後まで実行されると、そのスタックは破棄されます。それで、 スタックの数と寿命 ダイナミックであり、 OS レベルのスレッドの数によって決まるわけではありません。

私が「」と言ったことに注意してください。いつもの 関数ごとに個別のスタックが必要です。」両方あります 積み重なった そして スタックレス コルーチンの実装。最も注目すべきスタックフル C++ 実装は次のとおりです。 Boost.Coroutine そして マイクロソフト PPLさんの async/await. 。(ただし、C++ の 再開可能な機能 (別名」async そして await") は C++17 に提案されており、スタックレス コルーチンを使用する可能性があります。)

C++ 標準ライブラリへの Fibers の提案は近日中に行われます。また、サードパーティ製のものもあります 図書館. 。グリーン スレッドは、Python や Ruby などの言語で非常に人気があります。

主要な点はすでに説明されていますが、共有したいことがあります。

スタック

  • 非常に高速なアクセス。
  • RAMに保存されます。
  • 関数呼び出しは、渡されたローカル変数および関数パラメーターとともにここにロードされます。
  • プログラムがスコープ外に出ると、スペースは自動的に解放されます。
  • シーケンシャルメモリに保存されます。

ヒープ

  • スタックに比べてアクセスが遅い。
  • RAMに保存されます。
  • 動的に作成された変数はここに保存されますが、使用後に割り当てられたメモリを解放する必要があります。
  • メモリ割り当てが行われる場所に格納され、常にポインタによってアクセスされます。

興味深いメモ:

  • 関数呼び出しがヒープに保存されていた場合、次の 2 つの厄介な点が発生するでしょう。
    1. スタックに順次格納されるため、実行が高速になります。ヒープに保存すると膨大な時間が消費され、プログラム全体の実行が遅くなります。
    2. 関数がヒープ (ポインタが指す乱雑なストレージ) に格納されている場合、呼び出し元のアドレスに戻る方法はありません (スタックはメモリ内に順次格納されるため、このアドレスが与えられます)。

多くの答えは概念としては正しいですが、ハードウェアにはスタックが必要であることに注意する必要があります(つまり、マイクロプロセッサ) を使用して、サブルーチンの呼び出しを許可します (アセンブリ言語の CALL..)。(OOP の人はそれを呼びます メソッド)

スタック上にリターンアドレスを保存し、call→push/ret→popはハードウェアで直接管理されます。

スタックを使用してパラメータを渡すことができます。たとえレジスタを使用するよりも遅い場合でも (マイクロプロセッサの第一人者が言うか、1980 年代の優れた BIOS 本が言うでしょうか...)

  • スタックなし いいえ マイクロプロセッサは動作できます。(たとえアセンブリ言語であっても、サブルーチン/関数のないプログラムは想像できません)
  • ヒープがなくてもそれは可能です。(アセンブリ言語プログラムは、ヒープが OS/Lib 呼び出しである malloc などの OS の概念であるため、動作しなくても動作します。

スタックの使用は次のように高速になります。

  • ハードウェアなのでプッシュ/ポップも非常に効率的です。
  • malloc では、カーネル モードに入り、ロック/セマフォ (または他の同期プリミティブ) を使用してコードを実行し、割り当てを追跡するために必要な構造を管理する必要があります。

おお!非常に多くの回答があり、そのうちの 1 つが正解していないと思います...

1) それらは (実際のコンピューターのメモリ内の物理的に) どこに、何があるのでしょうか?

スタックは、プログラム イメージに割り当てられた最上位のメモリ アドレスとして始まり、そこから値が減少するメモリです。これは、呼び出された関数パラメータと関数で使用されるすべての一時変数用に予約されています。

ヒープは 2 つあります。パブリックとプライベート。

プライベート ヒープは、プログラム内のコードの最後のバイトの後の 16 バイト境界 (64 ビット プログラムの場合) または 8 バイト境界 (32 ビット プログラムの場合) で始まり、そこから値が増加します。デフォルト ヒープとも呼ばれます。

プライベート ヒープが大きくなりすぎるとスタック領域と重複し、大きくなりすぎるとスタックがヒープと重複します。スタックは高いアドレスから開始され、低いアドレスに向かって進むため、適切なハッキングを行うと、スタックが非常に大きくなり、プライベート ヒープ領域をオーバーランしてコード領域と重複する可能性があります。重要なのは、コードにフックできるコード領域を十分にオーバーラップさせることです。これを行うのは少し難しく、プログラムがクラッシュする危険がありますが、簡単で非常に効果的です。

パブリック ヒープは、プログラム イメージ空間の外側にある独自のメモリ空間に存在します。メモリ リソースが不足すると、このメモリがハードディスクに吸い取られます。

2) OS または言語ランタイムによってどの程度制御されますか?

スタックはプログラマによって制御され、プライベート ヒープは OS によって管理され、パブリック ヒープは OS サービスであるため誰にも制御されません。要求を行うと、それらは許可または拒否されます。

2b) その範囲は何ですか?

これらはすべてプログラムに対してグローバルですが、その内容はプライベート、パブリック、またはグローバルにすることができます。

2c) それぞれのサイズは何によって決まりますか?

スタックとプライベート ヒープのサイズは、コンパイラの実行時オプションによって決まります。パブリック ヒープは、サイズ パラメーターを使用して実行時に初期化されます。

2d) 何が高速化するのでしょうか?

これらは高速になるように設計されているのではなく、便利になるように設計されています。プログラマがそれらをどのように利用するかによって、それらが「速い」か「遅い」かが決まります。

参照:

https://norasandler.com/2019/02/18/Write-a-Compiler-10.html

https://docs.microsoft.com/en-us/windows/desktop/api/heapapi/nf-heapapi-getprocessheap

https://docs.microsoft.com/en-us/windows/desktop/api/heapapi/nf-heapapi-heapcreate

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top