スタック領域の「大きさ」を理解する
― .NET/ネイティブ/組み込みまで“どのくらい確保されるの?”をまとめて解説 ―
目次
1. スタックサイズは環境ごとに違う
実行環境 | 既定の予約サイズ (x64) | 初期コミット | カスタマイズ方法 |
---|---|---|---|
.NET / .NET Core(メイン & スレッドプール) | 1 MB | 4 KB ずつ伸張 | COMPlus_DefaultStackSizeThreadPool.ThreadStackSizenew Thread(…, size) |
Windows ネイティブ EXE | PE ヘッダー既定 1 MB | /STACK:reserve,commitで変更 | link.exe /STACK |
POSIX (pthread) | glibc: 8 MB / musl: 128 KB | pthread_attr_setstacksize | OS・libc に依存 |
マイコン (Cortex-M 等) | 数 KB〜数十 KB | リンカスクリプト固定 | RAM が少ないため厳密管理 |
予約サイズ = 仮想アドレス空間をどれだけ押さえるか
コミットサイズ = 実メモリ(RAM)をいつ確保するか
2. なぜ上限を設けるのか?
理由 | 詳細 |
---|---|
アドレス空間節約 | 1 プロセスで数千スレッドを張るとき、無制限だと仮想メモリが枯渇する |
ガードページ配置 | 境界直下に保護ページを置き、オーバーフロー時に例外を発生させる |
キャッシュ局所性 | 小さいスタックは同一ページ内で完結し、TLB ミスが減る |
3. スタックが足りなくなる典型パターン
症状 | 主な原因 | 代表的な対策 |
---|---|---|
StackOverflowException(.NET) | 深い/無限再帰 | ループ化・末尾再帰最適化・スタック増量 |
スレッド生成に失敗 | 予約 1 MB × N が上限超過 | new Thread(…, 256 * 1024) などで縮小 |
ネイティブ呼び出しでクラッシュ | 巨大ローカル配列 (char buf[1<<20]) | ヒープ確保 (malloc, stackalloc 回避) |
4. .NET でスタックサイズを変える方法
// 個々のスレッドだけ変えたい
var th = new Thread(ThreadProc, stackSize: 512 * 1024); // 512 KB
th.Start();
// プロセス全体で既定を変えたい (.NET 6+)
{
"runtimeOptions": {
"configProperties": {
"ThreadPool.ThreadStackSize": 262144 // 256 KB
}
}
}
- 環境変数 COMPlus_DefaultStackSize でも一括変更可能
- Xamarin/iOS などモバイル向けテンプレートは初期値が 256 KB〜512 KB に下げられている
5. どのくらいが適切か?
アプリの特性 | 推奨スタック |
---|---|
UI スレッド / ASP.NET リクエスト | 既定 1 MB で十分 |
数千スレッドを張るバッチ処理 | 256 KB〜512 KB |
深再帰アルゴリズム(パーサ・DFS) | 2 MB 以上、または再帰→ループ化 |
マイコン / 組み込み | 数 KB〜数十 KB、オーバーフロー検知を入れる |
ポイント:実測が最優先
Process.GetCurrentProcess().Threads やパフォーマンスカウンタで 実際のコミット量 を見ながら調整すると安全です。
6. まとめ
- デフォルトは 1 MB(.NET, Windows)。環境によってはもっと小さい。
- スレッドを大量に使う/深い再帰を行う場合は 明示的に変更 する。
- スタック不足=StackOverflow だけでなく、スレッド作成失敗 の形で現れることもある。
- 予約を増やし過ぎると仮想メモリ枯渇、減らし過ぎるとオーバーフロー──適切なバランスが重要。
「速いけれど限られた作業台」をどう広げるか・どう節約するかが、マルチスレッドアプリの安定動作を左右します。
⚠️ 実行するとプロセスが強制終了します
学習・検証用 PC でのみお試しください(保存していない作業がある場合は必ず閉じてから実行を)。
.NET 6+ で StackOverflowException を再現する最短コード
// InfiniteRecursion.cs
using System;
class Program
{
static void Main()
{
Recurse(0);
}
// 再帰に終了条件を入れない(必ず深く潜る)
static void Recurse(int depth)
{
Console.WriteLine($"depth = {depth}");
Recurse(depth + 1); // ← 無限再帰
}
}
- dotnet new console -o StackOverflowDemo
- cd StackOverflowDemo && code .(または好きなエディタで開く)
- Program.cs を上記内容に書き換え
- dotnet run
期待される実行結果
depth = 15789
Process terminated. StackOverflowException.
表示される深さ(呼び出し回数)は環境によって変わりますが、
1 MB 既定スタックなら 10 万回弱 で溢れることが多いです。
“確実に” 少ない深さでオーバーフローさせたい場合
スレッドのスタックサイズを 小さく してから再帰を始めると、
数百〜数千回程度ですぐに例外に到達します。
using System;
using System.Threading;
class Program
{
static void Main()
{
// 128 KB スタックのワーカースレッドを生成
var t = new Thread(() => Recurse(0), 128 * 1024);
t.Start();
t.Join();
}
static void Recurse(int depth)
{
Recurse(depth + 1);
}
}
- 小さい組み込み環境や Unity/iOS で「なぜ StackOverflow が出るのか」を確かめるときに便利。
- 値型の大きなローカル変数(例:int[10000] buf;)を足すと、さらに速く溢れます。
なぜ StackOverflowException は捕まえられない?
- .NET では StackOverflowException が発生すると CLR がただちにプロセスを終了 させます。try–catch では回復できません。
- 理由:
- 例外オブジェクトを生成するための残りスタック領域がない
- オーバーフロー後はフレームが壊れており状態が不定
対策は「再帰をループに書き換える」「スタックサイズを増やす」「ローカル配列をヒープに移す」など、コード側で溢れないように設計するしかありません。
まとめ
- 無限再帰または深すぎる再帰が最もシンプルな StackOverflow の再現パターン。
- スレッドを 128 KB など小さめスタック にしておくと、少ない深さで安全にテストできる。
- .NET ではオーバーフロー発生=プロセス終了。回避策はコード修正のみ。
訪問数 7 回, 今日の訪問数 1回
ディスカッション
コメント一覧
まだ、コメントがありません