スタック変数-高速で、時にはデッド

不合格 C ++プログラムは、ローカル変数および一時変数にいわゆる自動ストレージを使用します。 通常、自動メモリはプログラムスタックの上に実装されるため、スタックと呼ばれます。 その大きな利点は、メモリの割り当てと解放が非常に高速であることです(通常は1つのプロセッサ命令)。 その大きなマイナスは比較的少量であり、この量を超えるメモリを割り当てようとすると、いわゆるスタックオーバーフローが発生し、プログラムがクラッシュします。



このことから制限が続きます-スタックにあまり多くのメモリを割り当てないでください。 多すぎる? これはいくらですか? 答えは、一見すると思うほど明白ではありません。







多すぎる-入手可能な範囲を超えています。 声明は完璧に正確ですが、あまり有用ではありません。



利用可能な金額-さまざまな要因によって異なります。 たとえば、Windowsでは、プログラムのメインスレッドの場合、リンカーメモリの設定でスタックメモリの量が指定され、残りのスレッドの場合はそれらが作成されます。 コードは別のプログラム(Webサーバーのモジュール)で実行され、スタックサイズはWebサーバーによって設定されます(IISはユーザーコードのスレッドスタックを256キロバイトに制限しますが、Windowsのデフォルトサイズは1メガバイトです)。 他のシステムでは、システムレベルの制限がある場合があります。 いずれにせよ、スタックサイズはそれほど大きくなく、スタック変数に使用するコードに常に依存するとは限りません。



使用されるスタックの量も開発者次第ではありません。



まず、コードはそれ自体で実行されることはありません-それは誰かによって実行されます。 いくつかのエントリポイントがあります。これは、制御が渡されるユーザー定義関数です。 エントリポイントには、チェーンに沿って相互に呼び出す関数がたくさんある可能性があり、その結果、そのうちの1つがエントリポイントと呼ばれます。 また、スタックの枯渇に貢献する可能性があり、開発者が予想するよりも最大許容量の残りが少なくなります。



第二に、ユーザーコードであっても、使用されているスタックメモリの量はそれほど明確ではありません。 読者は激怒します-それは明らかではありませんか? すべてのローカル変数のメモリ量を取得して計算するのは非常に簡単です。 たとえば、ここに...



 //sample 0 void trivial() { char buffer[4 * 1000 * 1000] = {}; MessageBoxA( 0, buffer, buffer, 0 ); }
      
      











...明らかに、4メガバイトのメモリが使用され、Windows(デフォルトのスタックサイズ)では、これは間違いなく飛ぶことはありません。



それでは、例はより複雑です。 ideone.comにアクセスします。 免責事項:ideone.comのC ++で投稿を書いている時点では、gcc-4.3.4が使用されていましたが、他のバージョンのgccでは動作が異なる場合があります。 次のコードを試します。



 //sample 1 #include <stdio.h> int main() { char buffer[7 * 1000 * 1000] = {}; printf( "%s", buffer ); }
      
      







結果は成功です。 OK、今これを試してください:



 //sample 2 #include <stdio.h> #include <stdlib.h> int main() { if( rand() ) { char buffer[7 * 1000 * 1000] = {}; printf( "%s", buffer ); } else { char buffer[6 * 1000 * 1000] = {}; printf( "%s", buffer ); } }
      
      





非常に予期しない



結果はランタイムエラーです。 どうした rand()の呼び出しはスタックを使い果たしましたか?



それは非常に単純です-ほとんどのC ++実装では、関数の操作の最初に、この関数に必要なスタックメモリの全量が一度に割り当てられます。コンパイラーは、メモリーに配置することを決定した変数(一部の変数はレジスターにマップされ、一部はデッドコードに表示されて単純に削除される場合があります)およびその順序に基づきます。



2番目の例では、コンパイラーは両方の配列にメモリーを割り当てる必要があると判断しました。これらの配列は分岐演算子の相互に排他的な分岐に割り当てられることを無視しました。 この規格では、この動作が許可されています(ISO / IEC 14882の3.7節:2003(E)は、「最短寿命」を指します)。



特に、特定の関数のスタックメモリ消費量は、コンパイラがその関数の変数によって占有されているメモリを再利用できるかどうかに依存します。 たとえば、2番目の例ではgcc-4.3.4が失敗しました。 Visual C ++ 10は2番目の例を処理しますが、この場合は処理しません(スタックサイズが1メガバイトであると想定されます)。



 //sample 3 class Temp { public: Temp() { memset( buffer, 0, sizeof( buffer ) ); printf( "%S", buffer ); } void Process() { printf( "%S", buffer ); } private: WCHAR buffer[300 * 1024]; }; int _tmain(int argc, _TCHAR* argv[]) { switch( rand() ) { case 1: Temp().Process(); break; case 2: Temp().Process(); break; default: break; } }
      
      







ここで、読者は「スタック上のそのような大きなオブジェクトにメモリを割り当てることは何もない」と反対し、これは絶対に真実でまったく役に立たないステートメントになります。 現実の世界では、愚かな理由でスタックが枯渇します。



たとえば、19個のブランチを持つスイッチがあり、それぞれに上記の例のように一時オブジェクトが作成されました。 コードは何年も働きました。 次に、一時オブジェクトを含む20番目のブランチを追加しました。 関数呼び出し56がすでに使い果たされる前のスタックは、たとえば256キロバイトでした。 スタックを完全に使い果たすには、各一時オブジェクトが平均20キロバイトを少し超えるだけで十分です。これは、メガバイトスタックに慣れているWindows開発者の観点からはそれほど明白ではありません。 20キロバイト-それでも「優秀な開発者はスタックに割り当てません」? スタックがほとんど使い果たされている場合は、2キロバイトのオブジェクトで十分です。



何ができますか? 3つのオプション。



オプション1は、スタックから「大きな」オブジェクトを作成することです。 明らかな代替手段は動的メモリです。 彼女には2つの問題があります。 まず、オブジェクトを適切なタイミングで適切に削除する必要がありますが、このバイクを発明する必要はありません。スマートポインターがあります。 第二に、動的メモリの割り当てと解放がはるかに遅い-プロセッサの命令を1つだけでは済まないので、少なくともこのような移行が著しく遅くなるかどうかを評価するか、疑わしい場合にこのコードをプロファイリングする価値があります。



オプション2はオブジェクトを減らすことです。 256キロバイトの配列を使用する必要はないかもしれませんが、4キロバイトの配列を使用できますか? これは常に可能とは限らず、オーバーフローのリスクを排除するものではありませんが、多くの場合、オーバーフローを大幅に削減します。



オプション番号3は、機能を複数に分割し、重複しないライフタイムを持つオブジェクトのメモリが異なる機能から割り当てられるようにすることです。



開発者が期待するように、コンパイラが常にスタック変数にメモリを割り当てるとは限らないという事実に備えてください。 ダーウィン賞の受賞は、長い間それほど簡単ではありませんでした。



ドミトリー・メッシェリャコフ、



開発者製品部門



All Articles