🦔

switch文は何をしているのか? (C#)


switch 文(式)、使ってますか?

はじめに:switch は何をしているのか?if と何が違うのか

私たちが日常的に使う switch(C#では switch 文/式)は、見た目は分岐を整理してくれる「きれいな」構文ですが、内部では何が起きているのでしょうか。if と何が違うのか、そしてどのように最適化されているのかを、IL(Intermediate Language) を覗きながら確認してみます。


.NET の実行モデルと IL

C#(や F#、VB、C++/CLI など)のソースコードは、まず IL にコンパイルされ、その後 JIT(Just-In-Time) コンパイラや AOT(Ahead-Of-Time) コンパイラを通じて、最終的に各プラットフォームで実行可能なバイナリへと変換されます。
一度中間言語(IL)を挟むことで、CPU やプラットフォームの違いを吸収しやすくなり、最適化の余地も広がります。


IL を見てみよう(SharpLab)

switch の挙動を知るには、C#コードを IL へ変換し結果を可視化できるツール(SharpLab など)を使うのが手っ取り早いです。以下は、引数の int に応じて "0"〜"3" を返すシンプルな例です。

C#(概念例)

public string SampleSwitch(int arg)
{
    switch (arg)
    {
        case 0: return "0";
        case 1: return "1";
        case 2: return "2";
        case 3: return "3";
    }
    return "";
}

SharpLab で見ると

IL のポイント(要約)

  • 引数を一度ローカル変数へ退避(stloc.1stloc.0 など)し、評価 した値を使って分岐します。
  • ジャンプ先アドレスを配列(テーブル)として定義 し、評価結果を インデックス としてジャンプ先を決定します。
  • 「ジャンプ先を記録したテーブル(ジャンプテーブル)」があるため、配列アクセスは O(1)。単純な if の連鎖より効率的に動けるのが本質です。

この構造が、switch の「読みやすさ」以上の強みになっていることが分かります。


C#に置き直すとこんなイメージ

IL のジャンプテーブル構造を、C#での擬似コードに置き直すと以下のようなイメージになります。

public string SampleSwitch_(int arg)
{
    // ローカル変数へ代入して評価
    var localArg = arg;

    // 「ジャンプ先」を関数配列で擬似表現
    var funcs = new Func<string>[] {
        () => "0",
        () => "1",
        () => "2",
        () => "3",
    };

    if (0 <= localArg && localArg <= 3)
    {
        // 配列アクセス(O(1))で「返り値取得処理」に到達
        return funcslocalArg;
    }

    return "";
}

実際の JIT 最適化はもっと賢いですが、概念的には「値 → インデックス → ジャンプ先」という流れです。


連番の整数に対する最適化:ジャンプテーブル

密な(連続した)整数キー に対する switch は、先ほどのような ジャンプテーブル による高速化が行われます。
このときの計算量は、配列アクセス+分岐 で非常に小さく、単純な if 連鎖より有利になります。


疎な整数や負の値を含む場合は?

では、キーが疎(例:1, 10, 100, 1000)だったり、負の値を含んだりする場合はどうでしょうか。

IL を確認すると、大小比較+等値比較 を組み合わせた分岐に展開されており、おおむね二分探索風 の形になります。C#での擬似表現は次の通りです。

public string SampleSwitch_(int arg)
{
    var localArg = arg;

    if (localArg > 10)
    {
        if (localArg == 100)  return "100";
        else if (localArg == 1000) return "1000";
    }

    if (localArg == 1)  return "1";
    if (localArg == 10) return "10";

    return "";
}

この分岐は「およそ二分探索的」な形で、計算量は O(log n) に近い動きになります。疎なキー集合では、テーブル化よりも比較の組み合わせで効率を確保する、という方針が見て取れます。


文字列の switch はどうなる?

次に 文字列キーswitch を覗いてみると、IL では System.String::op_Equality を使った 等値比較の連鎖 へ素直に展開されていることが分かります。

C#のイメージ

public string SampleSwitch_(string arg)
{
    var localArg = arg;

    if (localArg == "one")   return "1";
    if (localArg == "two")   return "2";
    if (localArg == "three") return "3";
    if (localArg == "four")  return "4";

    return "";
}

つまり、文字列 switch はほぼ単純な if 連鎖 と同様の形へ変換されます(肩書き・型パターンの switch も同様で、isinst 等による型チェックの直列化に近い構造です)。


「型」パターンの switch

型パターンの例では、IL 上で isinst による型判定が順に行われ、該当した型に応じて返り値をセットしています。これも、判定の直列化 に近い構造です。


学び:見た目はきれい、しかし中身は…

switch は構文としては読みやすく「きれい」ですが、実際の処理はケースによって泥臭い 最適化(あるいは単純な展開)になります。
それでも、密な整数キー のように、ジャンプテーブル が効く場面では強力です。逆に、大量のケース で最適化されないパターンを延々と回すと、当然ながら影響は出ます。


あえて言えば:別の手段を検討する余地

  • 大量の文字列 switch を高頻度で回すなら、Dictionary<string, T> の利用を検討できます。TryGetValue は平均的に高速で、ケースの増減にも強いです。

    public string DictionarySwitch(string arg)
    {
        var table = new Dictionary<string, string>()
        {
            { "a", "A" },
            { "b", "B" },
            { "c", "C" },
            // …いっぱい
        };
    
        if (table.TryGetValue(arg, out var result))
        {
            return result;
        }
        return "";
    }
    
  • 型パターン switch を多用するなら、インターフェース で責務を委譲したり、(純粋)仮想関数 へ切り出してディスパッチさせる設計も有効です。

    public interface IStringConvert
    {
        string Convert(string arg);
    }
    
    private IStringConvert _converter = new StringConvertA();
    
    public string InterfaceSwitch(string arg)
    {
        return _converter.Convert(arg);
    }
    

これらは、コンパイラの展開/最適化に過度に依存しない ための選択肢として考えると楽しいはずです。


まとめ

  • switch密な整数キー では ジャンプテーブル により O(1) に近い効率で動作します。
  • 疎な整数集合(負の値を含むなど)では、大小比較+等値比較を組み合わせた 二分探索的な形(O(log n)) に展開されます。
  • 文字列の switch は多くの場合、等値比較の連鎖 へ素直に展開されます。
  • 見た目がきれいでも、内部の処理は必ずしも美しく最適化されるわけではありません。大量のケース を高頻度で回す場面では、Dictionary やインターフェース/仮想関数 への置き換えを検討すると良いです。

結局のところ、「switch は何をしているのか」を知っておくと、適材適所の選択 ができるようになります。場面に応じて、構文と実装(IL/JIT の展開)をイメージしながら使い分けていきたいところです。

Thinkingsテックブログ

Discussion