スタック領域の「大きさ」を理解する

― .NET/ネイティブ/組み込みまで“どのくらい確保されるの?”をまとめて解説 ―


1. スタックサイズは環境ごとに違う

実行環境既定の予約サイズ (x64)初期コミットカスタマイズ方法
.NET / .NET Core(メイン & スレッドプール)1 MB4 KB ずつ伸張COMPlus_DefaultStackSizeThreadPool.ThreadStackSizenew Thread(…, size)
Windows ネイティブ EXEPE ヘッダー既定 1 MB/STACK:reserve,commitで変更link.exe /STACK
POSIX (pthread)glibc: 8 MB / musl: 128 KBpthread_attr_setstacksizeOS・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);   // ← 無限再帰
    }
}
  1. dotnet new console -o StackOverflowDemo
  2. cd StackOverflowDemo && code .(または好きなエディタで開く)
  3. Program.cs を上記内容に書き換え
  4. 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 では回復できません。
  • 理由:
    1. 例外オブジェクトを生成するための残りスタック領域がない
    2. オーバーフロー後はフレームが壊れており状態が不定

対策は「再帰をループに書き換える」「スタックサイズを増やす」「ローカル配列をヒープに移す」など、コード側で溢れないように設計するしかありません。


まとめ

  • 無限再帰または深すぎる再帰が最もシンプルな StackOverflow の再現パターン。
  • スレッドを 128 KB など小さめスタック にしておくと、少ない深さで安全にテストできる。
  • .NET ではオーバーフロー発生=プロセス終了。回避策はコード修正のみ
訪問数 7 回, 今日の訪問数 1回

C#,メモリ管理

Posted by hidepon