プログラミング言語の本では、値型はスタック上に作成され、参照型はヒープ上に作成されると説明されていますが、この 2 つが何であるかは説明されていません。私はこれについての明確な説明を読んだことがありません。スタックとは何かを理解しています。しかし、
- それらはどこにあり、何ですか (物理的には実際のコンピューターのメモリ内にあります)。
- それらは OS または言語ランタイムによってどの程度制御されますか?
- 彼らの範囲は何ですか?
- それぞれの大きさは何で決まるのですか?
- 何が速くなるの?
プログラミング言語の本では、値型はスタック上に作成され、参照型はヒープ上に作成されると説明されていますが、この 2 つが何であるかは説明されていません。私はこれについての明確な説明を読んだことがありません。スタックとは何かを理解しています。しかし、
スタックは、実行スレッドのスクラッチ スペースとして確保されるメモリです。関数が呼び出されると、ローカル変数と一部の簿記データ用にスタックの上部にブロックが予約されます。その関数が戻ると、ブロックは未使用になり、次に関数が呼び出されたときに使用できます。スタックは常に LIFO (後入れ先出し) 順で予約されます。最後に予約されたブロックは常に、次に解放されるブロックになります。これにより、スタックの追跡が非常に簡単になります。スタックからブロックを解放することは、1 つのポインターを調整することに他なりません。
ヒープは、動的割り当て用に確保されたメモリです。スタックとは異なり、ヒープからのブロックの割り当てと割り当て解除に強制的なパターンはありません。いつでもブロックを割り当て、いつでも解放できます。これにより、任意の時点でヒープのどの部分が割り当てられているか、または解放されているかを追跡することがはるかに複雑になります。さまざまな使用パターンに合わせてヒープ パフォーマンスを調整するために利用できるカスタム ヒープ アロケータが多数あります。
各スレッドはスタックを取得しますが、通常、アプリケーションには 1 つのヒープしかありません (ただし、さまざまな種類の割り当てに複数のヒープがあることは珍しくありません)。
質問に直接答えるには:
それらは OS または言語ランタイムによってどの程度制御されますか?
OS は、スレッドが作成されるときに、システム レベルのスレッドごとにスタックを割り当てます。通常、OS は言語ランタイムによって呼び出され、アプリケーションにヒープを割り当てます。
彼らの範囲は何ですか?
スタックはスレッドにアタッチされているため、スレッドが終了するとスタックは回収されます。ヒープは通常、ランタイムによってアプリケーションの起動時に割り当てられ、アプリケーション (技術的にはプロセス) が終了すると回収されます。
それぞれの大きさは何で決まるのですか?
スタックのサイズは、スレッドの作成時に設定されます。ヒープのサイズはアプリケーションの起動時に設定されますが、領域が必要になると大きくなる可能性があります (アロケーターはオペレーティング システムからより多くのメモリを要求します)。
何が速くなるの?
スタックは、アクセス パターンによってメモリの割り当てと割り当て解除が簡単になるため高速です (ポインター/整数は単純にインクリメントまたはデクリメントされます)。また、スタック内の各バイトは非常に頻繁に再利用される傾向があるため、プロセッサのキャッシュにマップされる傾向があり、非常に高速になります。ヒープのもう 1 つのパフォーマンス ヒットは、大部分がグローバル リソースであるヒープが、通常、マルチスレッド セーフである必要があることです。つまり、各割り当てと割り当て解除は、通常、プログラム内の他の「すべての」ヒープ アクセスと同期する必要があります。
明確なデモンストレーション:
画像の出典: vikashazrati.wordpress.com
スタック:
ヒープ:
delete
データは、delete[]
、またはで解放されfree
ます。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;
最も重要な点は、ヒープとスタックは、メモリを割り当てる方法の総称であるということです。それらはさまざまな方法で実装でき、用語は基本的な概念に適用されます。
アイテムのスタックでは、アイテムはそこに配置された順序で他のアイテムの上に置かれ、一番上のアイテムのみを削除できます (全体を倒す必要はありません)。
スタックの単純さは、割り当てられたメモリの各セクションのレコードを含むテーブルを維持する必要がないことです。必要な唯一の状態情報は、スタックの末尾への単一のポインターです。割り当てと割り当て解除を行うには、その単一のポインターをインクリメントおよびデクリメントするだけです。注: スタックは、メモリーのセクションの最上部から開始し、上に成長するのではなく、下に拡張するように実装できる場合があります。
ヒープでは、アイテムの配置方法に特定の順序はありません。明確な「トップ」アイテムがないため、任意の順序でアイテムに手を伸ばして削除できます。
ヒープの割り当てには、割り当てられているメモリと割り当てられていないメモリの完全な記録を維持する必要があります。また、断片化を減らしたり、要求されたサイズに収まる十分な大きさの連続したメモリ セグメントを見つけたりするためのオーバーヘッド メンテナンスも必要です。空き領域を残して、いつでもメモリの割り当てを解除できます。メモリ アロケータは、割り当てられたメモリを移動することによるメモリの最適化やガベージ コレクションなどのメンテナンス タスクを実行することがあります。
これらのイメージは、スタックとヒープでメモリを割り当てて解放する 2 つの方法をかなりうまく説明しているはずです。うーん!
それらは OS または言語ランタイムによってどの程度制御されますか?
前述のように、ヒープとスタックは一般的な用語であり、さまざまな方法で実装できます。通常、コンピューター プログラムには、呼び出し元の関数へのポインターやローカル変数など、現在の関数に関連する情報を格納する呼び出しスタックと呼ばれるスタックがあります。関数は他の関数を呼び出してから戻るため、スタックは拡大および縮小して、関数からの情報をコール スタックのさらに下に保持します。プログラムは実際にはランタイム制御を持っていません。それは、プログラミング言語、OS、さらにはシステム アーキテクチャによって決まります。
ヒープは、動的かつランダムに割り当てられるメモリを指す一般的な用語です。すなわち故障。通常、メモリは OS によって割り当てられ、アプリケーションは API 関数を呼び出してこの割り当てを行います。動的に割り当てられたメモリを管理するにはかなりのオーバーヘッドが必要ですが、これは通常、使用するプログラミング言語または環境のランタイム コードによって処理されます。
彼らの範囲は何ですか?
コール スタックは非常に低レベルの概念であるため、プログラミングの意味での「スコープ」とは関係ありません。一部のコードを逆アセンブルすると、スタックの一部への相対ポインター スタイルの参照が表示されますが、高レベル言語に関する限り、言語は独自のスコープ ルールを課します。ただし、スタックの重要な側面の 1 つは、関数が戻ると、その関数にローカルなものはすべてスタックからすぐに解放されることです。これは、プログラミング言語がどのように機能するかを考えると、期待どおりに機能します。ヒープでは、定義することも困難です。スコープは OS によって公開されるものですが、プログラミング言語はおそらく、アプリケーションの「スコープ」とは何かに関する規則を追加します。プロセッサ アーキテクチャと OS は仮想アドレッシングを使用し、これはプロセッサが物理アドレスに変換し、ページフォールトなどがあります。どのページがどのアプリケーションに属しているかを追跡します。ただし、これについて本当に心配する必要はありません。プログラミング言語がメモリの割り当てと解放に使用する方法を使用し、エラーをチェックするだけなので (何らかの理由で割り当て/解放が失敗した場合)。
それぞれの大きさは何で決まるのですか?
繰り返しますが、言語、コンパイラ、オペレーティング システム、およびアーキテクチャによって異なります。スタックは通常、事前に割り当てられます。これは、定義上、連続したメモリでなければならないためです。言語コンパイラまたは OS がそのサイズを決定します。スタックに大量のデータを保存しないため、不要な無限再帰 (したがって「スタック オーバーフロー」) やその他の異常なプログラミングの決定が発生した場合を除いて、完全に使用されることはありません。
ヒープとは、動的に割り当てることができるあらゆるものの総称です。見方によっては、常にサイズが変化しています。最新のプロセッサとオペレーティング システムでは、それが機能する正確な方法はいずれにせよ非常に抽象化されているため、通常は深いところでどのように機能するかについてあまり心配する必要はありません。まだ割り当てていないか、解放したメモリ。
何が速くなるのですか?
すべての空きメモリが常に連続しているため、スタックは高速です。空きメモリのすべてのセグメントのリストを維持する必要はなく、スタックの現在のトップへの単一のポインタだけです。コンパイラは通常、この目的のために、このポインタを特別な高速レジスタに格納します。さらに、スタックに対する後続の操作は、通常、メモリの非常に近い領域に集中します。これは、非常に低いレベルでは、プロセッサのオンダイ キャッシュによる最適化に適しています。
(この回答は、多かれ少なかれこの質問のだまされた別の質問から移動しました。)
あなたの質問に対する答えは実装固有のものであり、コンパイラやプロセッサのアーキテクチャによって異なる場合があります。ただし、ここでは簡単に説明します。
new
またはによる)ヒープ上の新しい割り当てが満たされます。malloc
これには、ヒープ上のブロックのリストを更新する必要があります。ヒープ上のブロックに関するこのメタ情報も、多くの場合、すべてのブロックのすぐ前の小さな領域にヒープに保存されます。スタックではなくヒープに関数を割り当てることはできますか?
いいえ、関数 (つまり、ローカル変数または自動変数) のアクティベーション レコードは、これらの変数を格納するためだけでなく、ネストされた関数呼び出しを追跡するためにも使用されるスタックに割り当てられます。
ヒープがどのように管理されるかは、実行環境次第です。Cmalloc
と C++ は を使用しますnew
が、他の多くの言語にはガベージ コレクションがあります。
ただし、スタックは、プロセッサ アーキテクチャに密接に関連する、より低レベルの機能です。十分なスペースがない場合にヒープを拡大することは、ヒープを処理するライブラリ呼び出しで実装できるため、それほど難しくありません。ただし、スタック オーバーフローは手遅れになって初めて発見されるため、多くの場合、スタックを増やすことは不可能です。実行スレッドをシャットダウンすることが唯一の実行可能なオプションです。
次の C# コードでは
public void Method1()
{
int i = 4;
int y = 2;
class1 cls1 = new class1();
}
メモリの管理方法は次のとおりです。
Local Variables
関数の呼び出しがスタックにある間だけ持続する必要があります。ヒープは、実際には有効期間が不明な変数に使用されますが、しばらく続くと予想されます。ほとんどの言語では、変数をスタックに格納する場合、コンパイル時に変数の大きさを知ることが重要です。
オブジェクト (更新するとサイズが変化します) は、作成時にどれくらいの期間存続するかわからないため、ヒープに置かれます。多くの言語では、参照がなくなったオブジェクト (cls1 オブジェクトなど) を見つけるためにヒープがガベージ コレクションされます。
Java では、ほとんどのオブジェクトがヒープに直接移動します。C / C++ などの言語では、ポインタを扱っていないときに構造体とクラスがスタックに残ることがよくあります。
詳細については、次を参照してください。
スタックとヒープのメモリ割り当ての違い « timmurphy.org
そしてここ:
この記事は上の画像のソースです: 6 つの重要な .NET の概念: スタック、ヒープ、値の型、参照型、ボックス化、ボックス化解除 - CodeProject
ただし、不正確な情報が含まれている可能性があることに注意してください。
スタック 関数を呼び出すと、その関数への引数とその他のオーバーヘッドがスタックに置かれます。いくつかの情報(帰りにどこに行くかなど)もそこに保存されます。関数内で変数を宣言すると、その変数もスタックに割り当てられます。
スタックの割り当て解除は、割り当てとは逆の順序で常に割り当て解除されるため、非常に簡単です。関数に入るとスタックのものが追加され、関数を出ると対応するデータが削除されます。これは、他の多くの関数を呼び出す(または再帰的なソリューションを作成する)多くの関数を呼び出さない限り、スタックの小さな領域内にとどまる傾向があることを意味します。
ヒープヒープ は、作成したデータをその場で配置する場所の総称です。プログラムが作成する宇宙船の数がわからない場合は、新しい(またはmallocまたは同等の)演算子を使用して各宇宙船を作成する可能性があります。この割り当てはしばらく続くため、作成した順序とは異なる順序で解放される可能性があります。
したがって、ヒープははるかに複雑になります。これは、未使用のメモリ領域がチャンクとインターリーブされてしまうためです。メモリは断片化されます。必要なサイズの空きメモリを見つけるのは難しい問題です。これが、ヒープを避ける必要がある理由です(ただし、まだ頻繁に使用されています)。
実装 スタックとヒープの両方の実装は、通常、ランタイム/OSに依存します。多くの場合、パフォーマンスが重要なゲームやその他のアプリケーションは、ヒープから大量のメモリを取得し、それを内部でディッシュして、メモリをOSに依存しないようにする独自のメモリソリューションを作成します。
これは、メモリ使用量が標準とはまったく異なる場合にのみ実用的です。つまり、ある巨大な操作でレベルをロードし、別の巨大な操作で全体をチャックできるゲームの場合です。
メモリ内の物理的な場所これは、仮想メモリと呼ばれるテクノロジにより、物理データが別の場所(ハードディスク上でも!)にある特定のアドレスにアクセスできるとプログラムに思わせる ため、あなたが考えるよりも関連性が低くなります。スタック用に取得するアドレスは、呼び出しツリーが深くなるにつれて昇順になります。ヒープのアドレスは予測不可能であり(つまり、実装固有)、率直に言って重要ではありません。
他の人は大まかなストロークに非常によく答えているので、いくつかの詳細を紹介します.
スタックとヒープは特異である必要はありません。複数のスタックがある一般的な状況は、プロセスに複数のスレッドがある場合です。この場合、各スレッドには独自のスタックがあります。複数のヒープを使用することもできます。たとえば、一部の DLL 構成では、異なるヒープから異なる DLL が割り当てられる可能性があります。そのため、別のライブラリによって割り当てられたメモリを解放することは、通常はお勧めできません。
Cでは、ヒープに割り当てる alloc とは対照的に、スタックに割り当てるallocaを使用することで、可変長割り当ての利点を得ることができます。このメモリは return ステートメントには耐えられませんが、スクラッチ バッファーには役立ちます。
あまり使用しないWindowsで巨大な一時バッファを作成するのは無料ではありません。これは、スタックが存在することを確認するために、関数が入力されるたびに呼び出されるスタック プローブ ループをコンパイラが生成するためです (Windows は、スタックを拡張する必要がある場合を検出するために、スタックの最後にある単一のガード ページを使用するためです)。スタックの最後から 1 ページ以上離れたメモリにアクセスすると、クラッシュします)。例:
void myfunction()
{
char big[10000000];
// Do something that only uses for first 1K of big 99% of the time.
}
mmap()
他の人があなたの質問に直接答えていますが、スタックとヒープを理解しようとするときは、従来の UNIX プロセス (スレッドとベースのアロケーターを使用しない) のメモリ レイアウトを考慮すると役立つと思います。メモリ管理用語集のWeb ページには、このメモリ レイアウトの図があります。
スタックとヒープは、従来、プロセスの仮想アドレス空間の両端に配置されていました。スタックはアクセスされると、カーネルによって設定されたサイズまで自動的に拡張されます (これは で調整できますsetrlimit(RLIMIT_STACK, ...)
)。メモリ アロケータがbrk()
またはsbrk()
システム コールを呼び出すと、ヒープが大きくなり、物理メモリのより多くのページがプロセスの仮想アドレス空間にマッピングされます。
一部の組み込みシステムなど、仮想メモリのないシステムでは、スタックとヒープのサイズが固定されていることを除いて、多くの場合、同じ基本レイアウトが適用されます。ただし、他の組み込みシステム (Microchip PIC マイクロコントローラーに基づくものなど) では、プログラム スタックは、データ移動命令によってアドレス指定できないメモリの個別のブロックであり、プログラム フロー命令 (call、返却など)。Intel Itanium プロセッサなどの他のアーキテクチャには、複数のスタックがあります。この意味で、スタックは CPU アーキテクチャの要素です。
スタックはメモリの一部であり、「pop」(スタックから値を削除して返す)や「push」(スタックに値をプッシュする)など、いくつかの主要なアセンブリ言語命令を介して操作できますが、(サブルーチンを呼び出します-これはアドレスをプッシュしてスタックに戻します)そして戻ります(サブルーチンから戻ります-これはアドレスをスタックからポップしてそれにジャンプします)。これは、スタックポインタレジスタの下のメモリ領域であり、必要に応じて設定できます。スタックは、サブルーチンに引数を渡すため、およびサブルーチンを呼び出す前にレジスタの値を保持するためにも使用されます。
ヒープは、オペレーティングシステムによって、通常はmallocのようなシステムコールを介してアプリケーションに提供されるメモリの一部です。最新のOSでは、このメモリは呼び出しプロセスのみがアクセスできるページのセットです。
スタックのサイズは実行時に決定され、通常、プログラムの起動後には大きくなりません。Cプログラムでは、スタックは、各関数内で宣言されたすべての変数を保持するのに十分な大きさである必要があります。ヒープは必要に応じて動的に拡張されますが、OSは最終的に呼び出しを行います(多くの場合、ヒープはmallocによって要求された値よりも大きくなるため、少なくとも一部の将来のmallocはカーネルに戻る必要がありません。より多くのメモリを取得します。この動作は多くの場合カスタマイズ可能です)
プログラムを起動する前にスタックを割り当てているので、スタックを使用する前にmallocを実行する必要はありません。これは、わずかな利点です。実際には、仮想メモリサブシステムを備えた最新のオペレーティングシステムでは、ページの実装方法と保存場所が実装の詳細であるため、何が高速で何が低速になるかを予測することは非常に困難です。
他の多くの人がこの問題についてほぼ正しい答えをあなたに与えたと思います。
ただし、見落とされている詳細の 1 つは、「ヒープ」は実際にはおそらく「フリー ストア」と呼ばれるべきであるということです。この区別の理由は、元のフリー ストアが「二項ヒープ」と呼ばれるデータ構造で実装されていたためです。そのため、malloc()/free() の初期の実装からの割り当ては、ヒープからの割り当てでした。ただし、この現代では、ほとんどの無料ストアは、二項ヒープではない非常に精巧なデータ構造で実装されています。
スタックを使用すると、いくつかの興味深いことができます。たとえば、allocaのような関数があります (その使用に関する大量の警告を回避できると仮定します)。これは、メモリ用にヒープではなくスタックを特に使用する malloc の形式です。
とはいえ、スタックベースのメモリ エラーは、私が経験した最悪のエラーの 1 つです。ヒープ メモリを使用していて、割り当てられたブロックの範囲を超えると、セグメント フォールトが発生する可能性が十分にあります。(100% ではありません: ブロックは、以前に割り当てた別のブロックと偶発的に隣接している可能性があります。) しかし、スタック上に作成された変数は常に互いに隣接しているため、範囲外に書き込むと、別の変数の値が変更される可能性があります。自分のプログラムが論理法則に従わなくなったと感じるときはいつでも、それはおそらくバッファ オーバーフローであるということを学びました。
簡単に言えば、スタックはローカル変数が作成される場所です。また、サブルーチンを呼び出すたびに、プログラム カウンター (次のマシン命令へのポインター) と重要なレジスターが呼び出され、パラメーターがスタックにプッシュされることがあります。次に、サブルーチン内のすべてのローカル変数がスタックにプッシュされます (そしてそこから使用されます)。サブルーチンが終了すると、そのすべてがスタックからポップバックされます。PC とレジスタのデータが取得され、ポップされたとおりに元の場所に戻されるため、プログラムは順調に進むことができます。
ヒープは、動的メモリ割り当てが行われるメモリ領域です (明示的な「新規」または「割り当て」呼び出し)。これは、さまざまなサイズのメモリ ブロックとそれらの割り当て状態を追跡できる特別なデータ構造です。
「従来の」システムでは、RAM は、スタック ポインターがメモリの一番下から始まり、ヒープ ポインターが一番上から始まり、互いに向かって大きくなるように配置されていました。それらが重なっている場合は、RAM が不足しています。ただし、最新のマルチスレッド OS では機能しません。すべてのスレッドは独自のスタックを持つ必要があり、それらは動的に作成できます。
スタック
ヒープ
1980 年代、UNIX はうさぎのように広まり、大企業が独自に展開しました。エクソンは、歴史に失われた数十のブランド名と同様に、1つを持っていました。メモリをどのように配置するかは、多くの実装者の裁量に任されていました。
典型的な C プログラムは、brk() の値を変更することでメモリを増やすことができます。通常、HEAP はこの brk 値のすぐ下にあり、brk を増やすと利用可能なヒープの量が増えました。
単一の STACK は、通常、メモリの次の固定ブロックの先頭まで値を含まないメモリ領域である HEAP の下の領域でした。この次のブロックは、多くの場合、その時代の有名なハックの 1 つでスタック データによって上書きされる可能性がある CODE でした。
典型的なメモリ ブロックの 1 つは BSS (ゼロ値のブロック) で、あるメーカーの製品では誤ってゼロに設定されていませんでした。もう 1 つは、文字列や数値などの初期化された値を含む DATA です。3 つ目は、CRT (C ランタイム)、メイン、関数、およびライブラリを含む CODE です。
UNIX での仮想メモリの出現により、多くの制約が変更されました。これらのブロックが連続している必要がある、サイズが固定されている、または特定の方法で並べられている必要があるという客観的な理由はありません。もちろん、UNIX が Multics になる前は、これらの制約に悩まされることはありませんでした。これは、その時代のメモリ レイアウトの 1 つを示す回路図です。
プロセスが作成され、コードとデータがロードされた後、データが終了した直後に OS セットアップ ヒープが開始され、アーキテクチャに基づいてアドレス空間の一番上にスタックされます
より多くのヒープが必要な場合、OS は動的に割り当て、ヒープ チャンクは常に実質的に連続します。
およびLinux のシステム コールを参照brk()
してください。sbrk()
alloca()