C および C++ が大きなオブジェクトをスタックに格納する方法を理解しようとしています。通常、スタックは整数のサイズであるため、大きなオブジェクトがそこに格納される方法がわかりません。それらは単に複数のスタック「スロット」を占有しますか?
11 に答える
スタックとヒープは、あなたが思っているほど違いはありません!
確かに、一部のオペレーティング システムにはスタック制限があります。(それらのいくつかには厄介なヒープ制限もあります!)
しかし、これはもう 1985 年ではありません。
最近では、Linux を実行しています。
デフォルトのスタックサイズは 10 MB に制限されています。デフォルトのヒープサイズは無制限です。そのスタックサイズの制限を解除するのは非常に簡単です。(*咳* [tcsh]スタックサイズの制限を解除*咳* またはsetrlimit()。)
スタックとヒープの最大の違いは次のとおりです。
- スタック割り当ては、ポインターをオフセットするだけです (スタックが十分に大きくなった場合は、新しいメモリ ページを割り当てる可能性があります)。 ヒープは、データ構造を検索して適切なメモリ ブロックを見つける必要があります。(そして、おそらく新しいメモリページも割り当てます。)
- 現在のブロックが終了すると、スタックは範囲外になります。 delete/free が呼び出されると、ヒープは範囲外になります。
- ヒープが断片化する可能性があります。 スタックが断片化されることはありません。
Linux では、スタックとヒープの両方が仮想メモリによって管理されます。
割り当て時間に関しては、ひどく断片化されたメモリをヒープ検索しても、メモリの新しいページにマッピングすることはできません。 時間的な違いはごくわずかです。
OS によっては、マップされている新しいメモリ ページを実際に使用する場合にのみ発生することがよくあります ( malloc()の割り当て中ではありません!) (これは遅延評価です。)
( newは、おそらくそれらのメモリページを使用するコンストラクターを呼び出します...)
スタックまたはヒープで大きなオブジェクトを作成および破棄することで、VM システムをスラッシングできます。システムがメモリを再利用できるかどうかは、OS/コンパイラによって異なります。再利用されていない場合、ヒープはそれを再利用できる可能性があります。(その間、別のmalloc()によって再利用されていないと仮定します。) 同様に、スタックが再利用されない場合は、再利用されます。
スワップアウトされたページはスワップインする必要がありますが、それはあなたの最大の時間ヒットになるでしょう.
これらすべてのことの中で、メモリの断片化が最も心配です!
寿命(範囲外になるとき)は常に決定的な要因です。
しかし、プログラムを長時間実行すると、断片化によってメモリ フットプリントが徐々に増加します。絶え間ないスワップは、最終的に私を殺します!
追加するために修正:
男、私は甘やかされてしまった !
ここで何かが足し合わなかった...私は、*私*がベースから外れていると考えました。または他の誰もがそうでした。または、おそらく両方です。または、たぶん、どちらでもない。
答えが何であれ、私は何が起こっているのかを知らなければなりませんでした!
…長くなりそうです。我慢して...
私は過去 12 年間のほとんどを Linux で作業してきました。そしてその約 10 年前、さまざまな種類の Unix の下で。コンピューターに対する私の見方はやや偏っています。甘やかされてしまった!
私は Windows で少しやったことがありますが、正式に話すには十分ではありません。残念なことに、Mac OS/Darwin の場合も同様です... Mac OS/Darwin/BSD は十分に近いので、私の知識の一部は引き継がれます。
32 ビット ポインターを使用すると、4 GB (2^32) でアドレス空間が不足します。
実際には、STACK + HEAP の組み合わせは、他のものをそこにマッピングする必要があるため、通常は 2 ~ 4 GB に制限されます。
(共有メモリ、共有ライブラリ、メモリマップファイル、実行中の実行可能イメージは常に素晴らしいなどがあります。)
Linux/Unix/MacOS/Darwin/BSD では、実行時に必要な任意の値にHEAPまたはSTACKを人為的に制約することができます。しかし、最終的には厳しいシステム制限があります。
これは、 "limit"と"limit -h"の (tcsh での) 違いです。または(bashで)「ulimit -Sa」と「ulimit -Ha」の比較。または、プログラムで、struct rlimitのrlim_curとrlim_maxの比較。
楽しい部分に移ります。Martin York's Codeに関して。( Martinさん、ありがとう! 良い例です。試してみるのはいつでも良いことです!.)
Martin はおそらく Mac で実行しています。(かなり最近のものです。彼のコンパイラのビルドは私のものよりも新しいものです!)
確かに、彼のコードはデフォルトでは彼の Mac では実行されません。しかし、最初に"unlimit stacksize" (tcsh) または"ulimit -Ss unlimited" (bash) を呼び出すと、問題なく動作します。
問題の核心:
古い (廃止された) Linux RH9 2.4.x カーネル ボックスでテストし、大量のSTACK OR HEAPを割り当てます。いずれか 1 つだけで 2 ~ 3 GB に達します。(悲しいことに、マシンの RAM+SWAP は 3.5 GB を少し下回ります。これは 32 ビット OS です。実行中のプロセスはこれだけではありません。私たちは持っているもので間に合わせます...)
したがって、人工的なものを除いて、LinuxではSTACKサイズとHEAPサイズに制限はありません...
しかし:
Mac では、65532 キロバイトのハード スタックサイズ制限があります。それは、物事がメモリにどのように配置されるかに関係しています。
通常、理想化されたシステムは、メモリ アドレス空間の一方の端にSTACKがあり、もう一方の端にHEAPがあり、それらが相互に構築されていると考えられます。彼らが会うとき、あなたは記憶がありません。
Mac は、両側を制限する固定オフセットで共有システム ライブラリを間に挟んでいるように見えます。8 MiB (< 64 MiB) 程度のデータしか割り当てていないため、「unlimit stacksize」を指定して Martin York のコードを実行することもできます。しかし、彼はHEAPを使い果たすずっと前にSTACKを使い果たします。
私はLinuxを使用しています。私はしません。 ごめんね。こちらがニッケルです。より良いOSを入手してください。
Mac 用の回避策があります。しかし、それらは見苦しく乱雑になり、カーネルまたはリンカーのパラメーターを微調整する必要があります。
長い目で見れば、Apple が本当にばかげたことをしない限り、64 ビットのアドレス空間は、このスタック制限全体をすぐに時代遅れにするでしょう。
フラグメンテーションに移ります:
STACKに何かをプッシュする と、最後に追加されます。そして、現在のブロックが終了するたびに削除 (ロールバック) されます。
その結果、STACKに穴はありません。それはすべて、使用済みメモリの 1 つの大きな固体ブロックです。おそらく最後に少しだけ未使用のスペースがあり、すべて再利用の準備ができています.
対照的に、HEAPが割り当てられて解放されると、未使用のメモリ ホールが発生します。これらは、時間の経過とともにメモリフットプリントの増加につながる可能性があります。通常、コア リークが意味するものとは異なりますが、結果は同様です。
メモリの断片化は、 HEAPストレージを避ける理由にはなりません。コーディングするときに注意すべきことです。
SWAP SHRASHINGが表示されます:
- すでに大量のヒープが割り当てられている/使用中の場合。
- ばらばらの穴がたくさん散らばっている場合。
- また、多数の小さな割り当てがある場合。
次に、非常に多くの仮想メモリ ページに散在する、コードの小さなローカライズされた領域内ですべて使用される多数の変数が発生する可能性があります。(この 2k ページで 4 バイトを使用し、その 2k ページで 8 バイトを使用しているように、多くのページで ...)
つまり、プログラムを実行するには多数のページをスワップインする必要があります。または、常にページを交換したり、ページを交換したりします。(私たちはそれをスラッシングと呼んでいます。)
一方、これらの小さな割り当てがSTACKで行われた場合、それらはすべて連続したメモリ領域に配置されます。ロードする必要がある VM メモリ ページが少なくなります。(4+8+... < 2k で勝利)
補足: 私がこれに注意を喚起する理由は、私が知っている特定の電気技師が、すべてのアレイを HEAP に割り当てるよう主張したことに由来しています。グラフィックスの行列計算を行っていました。3 つまたは 4 つの要素配列の *LOT*。新規/削除を一人で管理するのは悪夢でした。クラスで抽象化されていても、それは悲しみを引き起こしました!
次のトピック。ねじ切り:
はい、スレッドはデフォルトで非常に小さなスタックに制限されています。
これは pthread_attr_setstacksize() で変更できます。スレッドの実装にもよりますが、複数のスレッドが同じ 32 ビット アドレス空間を共有している場合、スレッドごとの個別の大きなスタックが問題になります。 それだけの余地はありません!ここでも、64 ビット アドレス空間 (OS) への移行が役立ちます。
pthread_t threadData;
pthread_attr_t threadAttributes;
pthread_attr_init( & threadAttributes );
ASSERT_IS( 0, pthread_attr_setdetachstate( & threadAttributes,
PTHREAD_CREATE_DETACHED ) );
ASSERT_IS( 0, pthread_attr_setstacksize ( & threadAttributes,
128 * 1024 * 1024 ) );
ASSERT_IS( 0, pthread_create ( & threadData,
& threadAttributes,
& runthread,
NULL ) );
Martin York のStack Framesに関して:
あなたと私は、違うことを考えているのではないでしょうか?
スタック フレームについて考えるとき、コール スタックを思い浮かべます。各関数またはメソッドには、戻りアドレス、引数、およびローカル データで構成される独自のスタック フレームがあります。
スタック フレームのサイズに関する制限は見たことがありません。全体としてSTACKに制限がありますが、それはすべてのスタック フレームを組み合わせたものです。
Wiki に、スタック フレームに関する優れた図と議論があります。
最後に:
Linux/Unix/MacOS/Darwin/BSD では、上限(tcsh) またはulimit (bash)と同様に、最大STACKサイズの制限をプログラムで変更できます。
struct rlimit limits;
limits.rlim_cur = RLIM_INFINITY;
limits.rlim_max = RLIM_INFINITY;
ASSERT_IS( 0, setrlimit( RLIMIT_STACK, & limits ) );
MacでINFINITYに設定しようとしないでください...そして、使用する前に変更してください。;-)
参考文献:
- http://www.informit.com/content/images/0131453483/downloads/gorman_book.pdf
- http://www.redhat.com/magazine/001nov04/features/vm/
- http://dirac.org/linux/gdb/02a-Memory_Layout_And_The_Stack.php
- http://people.redhat.com/alikins/system_tuning.html
- http://pauillac.inria.fr/~xleroy/linuxthreads/faq.html
- http://www.kegel.com/stackcheck/
スタックはメモリの一部です。スタック ポインタは先頭を指します。値をスタックにプッシュし、ポップして取得できます。
たとえば、2 つのパラメーター (1 バイト サイズと 2 バイト サイズの別のパラメーター) で呼び出される関数がある場合、8 ビット PC があると仮定します)。
両方ともスタックにプッシュされ、スタック ポインターが上に移動します。
03: par2 byte2
02: par2 byte1
01: par1
関数が呼び出され、戻りアドレスがスタックに置かれます。
05: ret byte2
04: ret byte1
03: par2 byte2
02: par2 byte1
01: par1
OK、関数内には 2 つのローカル変数があります。2 バイトの 1 つと 4 バイトの 1 つです。これらの位置はスタック上で予約されていますが、最初にスタック ポインターを保存します。これにより、カウントアップによって変数の開始位置がわかり、カウントダウンによってパラメーターが検出されます。
11: var2 byte4
10: var2 byte3
09: var2 byte2
08: var2 byte1
07: var1 byte2
06: var1 byte1
---------
05: ret byte2
04: ret byte1
03: par2 byte2
02: par2 byte1
01: par1
ご覧のとおり、スペースが残っている限り、スタックに何でも置くことができます。そうしないと、このサイトにその名前を付ける現象が発生します。
Push
pop
命令は通常、ローカル スタック フレーム変数の格納には使用されません。関数の開始時に、関数のローカル変数に必要なバイト数 (ワード サイズに合わせて) だけスタック ポインターをデクリメントすることにより、スタック フレームが設定されます。これにより、これらの値に対して「スタック上」に必要な量のスペースが割り当てられます。すべてのローカル変数は、このスタック フレームへのポインターを介してアクセスされます ( ebp
x86 の場合)。
スタックは、ローカル変数、関数呼び出しから戻るための情報などを格納する大きなメモリ ブロックです。スタックの実際のサイズは、OS によって大きく異なります。たとえば、Windows で新しいスレッドを作成する場合、デフォルトのサイズは 1 MBです。
スタックで現在使用できるメモリよりも多くのメモリを必要とするスタック オブジェクトを作成しようとすると、スタック オーバーフローが発生し、問題が発生します。悪用コードの大規模なクラスは、これらまたは同様の状態を意図的に作成しようとします。
スタックは整数サイズのチャンクに分割されません。これは単なるバイト配列です。size_t 型 (int ではない) の「整数」によってインデックス付けされます。現在利用可能なスペースに収まる大きなスタック オブジェクトを作成すると、スタック ポインターを上に (または下に) バンプすることでそのスペースを使用するだけです。
他の人が指摘しているように、大きなオブジェクトにはスタックではなくヒープを使用するのが最善です。これにより、スタック オーバーフローの問題が回避されます。
編集: 64 ビット アプリケーションを使用していて、OS とランタイム ライブラリが適切である場合 (mrree の投稿を参照)、スタックに大きな一時オブジェクトを割り当てても問題ありません。アプリケーションが 32 ビットであったり、OS やランタイム ライブラリが適切でない場合は、おそらくこれらのオブジェクトをヒープに割り当てる必要があります。
関数を入力するたびに、スタックはその関数内のローカル変数に合わせて拡大します。largeObject
たとえば 400 バイトを使用するクラスの場合:
void MyFunc(int p1, largeObject p2, largeObject *p3)
{
int s1;
largeObject s2;
largeObject *s3;
}
この関数を呼び出すと、スタックは次のようになります (詳細は、呼び出し規約とアーキテクチャによって異なります)。
[... rest of stack ...]
[4 bytes for p1]
[400 bytes for p2]
[4 bytes for p3]
[return address]
[old frame pointer]
[4 bytes for s1]
[400 bytes for s2]
[4 bytes for s3]
スタックの動作に関する情報については、x86 Calling Conventionsを参照してください。MSDN には、サンプル コードと結果のスタック図を含む、いくつかの異なる呼び出し対流の優れた図もあります。
他の人が言ったように、「大きなオブジェクト」が何を意味するのかは明確ではありません...ただし、それから尋ねるので
それらは単に複数のスタック「スロット」を占有しますか?
単に整数より大きいものを意味していると推測します。ただし、他の誰かが指摘したように、スタックには整数サイズの「スロット」はありません。これは単なるメモリのセクションであり、その中のすべてのバイトには独自のアドレスがあります。コンパイラは、その変数の最初のバイトのアドレスによってすべての変数を追跡します。これは、アドレス演算子 (&var
)、ポインターの値は、他の変数のこのアドレスです。コンパイラは、すべての変数の型も認識しており (変数を宣言したときにそれを伝えました)、各型がどのくらいの大きさであるべきかを認識しています。変数は、関数が呼び出されるときに必要になり、その結果を関数のエントリポイント コード (PDaddy が言及したスタック フレーム) に含めます。
C および C++ では、(ご想像のとおり) スタックが制限されているため、大きなオブジェクトをスタックに格納しないでください。各スレッドのスタックは通常、数メガバイト以下です (スレッドの作成時に指定できます)。オブジェクトを作成するために「new」を呼び出すと、オブジェクトはスタックに置かれず、代わりにヒープに置かれます。
スタックサイズには制限があります。通常、スタック サイズはプロセスの作成時に設定されます。そのプロセスの各スレッドは、CreateThread() 呼び出しで特に指定されていない場合、デフォルトのスタック サイズを自動的に取得します。つまり、はい: 複数のスタック「スロット」が存在する可能性がありますが、各スレッドには 1 つしかありません。また、スレッド間で共有することはできません。
残りのスタック サイズよりも大きいオブジェクトをスタックに配置すると、スタック オーバーフローが発生し、アプリケーションがクラッシュします。
そのため、非常に大きなオブジェクトがある場合は、スタックではなくヒープに割り当てます。ヒープは、仮想メモリの量によってのみ制限されます (これはスタックよりも大きくなります)。
「スタックは整数のサイズです」とは、「スタックポインタは整数のサイズです」という意味です。これは、メモリの巨大な領域であるスタックの一番上を指します。まあ、整数より大きい。
スタックに置く意味がないほど巨大な (または数が多い) オブジェクトを持つことができます。その場合、オブジェクトをヒープに置き、それへのポインターをスタックに置くことができます。これは、値渡しと参照渡しの違いです。
ラージ オブジェクトをどのように定義しますか? 割り当てられたスタック領域のサイズよりも大きいか小さいかを話していますか?
たとえば、次のようなものがある場合:
void main() {
int reallyreallybigobjectonthestack[1000000000];
}
システムによっては、オブジェクトを保存するのに十分なスペースがないため、segfault が発生する可能性があります。それ以外の場合は、他のオブジェクトと同様に保存されます。実際の物理メモリで話している場合は、オペレーティング システム レベルの仮想メモリがそれを処理するため、これについて心配する必要はありません。
また、スタックのサイズは、オペレーティング システムとアプリケーションの仮想アドレス空間のレイアウトに完全に依存する整数のサイズではない可能性があります。