C# 9.0の範囲パターンのswitchはわりと賢くコンパイルされる

3 min read読了の目安(約3300字

範囲パターンを使ったswitch式

C# 9.0 で switch式に比較演算を指定できるようになりました。

元のコード
public class C
{
    public int Expr(int num) => num switch
    {
            <=0 => -1,
            < 10 => 0,
            < 20 => 1,
            < 30 => 2,
            < 40 => 3,
            < 50 => 4,
            < 60 => 5,
            _ => -1,
    };
}

上記のコードをコンパイル結果(のILをC#にデコンパイルしたもの)はこちら。

コンパイル結果
public class C
{
    public int Expr(int num)
    {
        if (num < 30)
        {
            if (num < 10)
            {
                if (num <= 0)
                {
                    return -1;
                }
                return 0;
            }
            if (num < 20)
            {
                return 1;
            }
            return 2;
        }
        if (num < 50)
        {
            if (num < 40)
            {
                return 3;
            }
            return 4;
        }
        if (num < 60)
        {
            return 5;
        }
        return -1;
    }
}

なんと二分探索になっています。

このくらいならばif文を列挙しても大差ないかとは思いますが、二分探索になることで if文が実行される回数が log_2(ケース数) 回程度になります。デメリットなしで高速化が期待できるので、範囲を列挙するような箇所では積極的にswitch式で書いてしまうのが良さそうです。

ちなみに、switch文でもジャンプテーブルにできないような場合は二分探索に変換されるようです。

http://engineering.grani.jp/entry/2017/02/20/175816

追記

「デメリットなしで」と書きましたが、「殆どの場合は 0 <= num && num < 10 である」というようにマッチする頻度が非常に高いものがあるならマッチする頻度が高い順にif文を羅列するほうが二分探索より高速そうです。。

orを使う場合

or も使ってみます。

元のコード
public class C
{
    public int Expr(int num) => num switch
    {
            <=0 => -1,
            < 10 or > 600 => 0,
            < 20 or > 500 => 1,
            < 30 or > 400 => 2,
            < 40 or > 300 => 3,
            < 50 or > 200 => 4,
            < 60 or > 100 => 5,
            _ => -1,
    };
}

コンパイル結果はこちら。

コンパイル結果
public class C
{
    public int Expr(int num)
    {
        if (num > 100)
        {
            if (num > 300)
            {
                if (num > 500)
                {
                    if (num > 600)
                    {
                        goto IL_0061;
                    }
                    goto IL_0065;
                }
                if (num > 400)
                {
                    goto IL_0069;
                }
                goto IL_006d;
            }
            if (num > 200)
            {
                goto IL_0071;
            }
        }
        else
        {
            if (num < 30)
            {
                if (num < 10)
                {
                    if (num > 0)
                    {
                        goto IL_0061;
                    }
                    return -1;
                }
                if (num < 20)
                {
                    goto IL_0065;
                }
                goto IL_0069;
            }
            if (num < 50)
            {
                if (num < 40)
                {
                    goto IL_006d;
                }
                goto IL_0071;
            }
            if (num >= 60)
            {
                return -1;
            }
        }
        return 5;
        IL_0065:
        return 1;
        IL_0061:
        return 0;
        IL_0071:
        return 4;
        IL_0069:
        return 2;
        IL_006d:
        return 3;
    }
}

gotoでスパゲッティ気味になっていますが、これも二分探索になっています。

結論

範囲チェックを列挙するときは switch式だと効率的!