「そういやブログに書いてなかった」ネタ。 Pull Request が通った プレビュー版(Visual Studio 16.6 Preview 1)でよければ今年の2月頃から使えてた話です。
文字列に対する switch に新しい最適化手法が導入されました。
元々の switch のコスト
例として以下のような switch を考えます。
static int StringSwitch(string s) => s switch { "abc" => 0, "def" => 1, "ghi" => 2, "01234a" => 3, "01234b" => 4, "01234c" => 5, "aaaaaaaa" => 6, _ => -1, };
C# コンパイラー的には、
case少なければ単に上から順にif (s == "...")を並べる- 多ければ IL の switch 命令を出力
みたいな処理をしていました。
ちなみに、IL の switch 命令は、
- 各
case文字列に対応するハッシュ値を事前計算 s.GetHashCode()でswitchする
みたいなコードを生成するそうです。
if (s == "...") を並べる方式はワーストケースでは多数の == 比較がかかりますし、
文字列に対する GetHashCode の計算は意外と重たい処理です。
新手法 switch
Visual Studio 17.6 (Roslyn 4.6) 以降では、case の数が中程度のとき、
以下のような分岐をかけるようになりました
(Trie木的発想の簡易アルゴリズム)。
- 文字列長でまず分岐
- どこか1文字だけを使って
charでswitch - その後に
stringの==判定
Length-based switch dispatch (文字列長ベースの switch 分配)というそうです。
先ほどの例の switch だと、おおむね以下のような感じの分岐に置き換わります。
(実際はもうちょっと goto だらけのコードになりますが、
見やすさ優先で変更。)
StringSwitch(""); static int StringSwitch(string s) => s.Length switch { 3 => s[0] switch { 'a' => s == "abc" ? 0 : -1, 'd' => s == "def" ? 1 : -1, 'g' => s == "ghi" ? 2 : -1, _ => -1, }, 6 => s[5] switch { 'a' => s == "01234a" ? 3 : -1, 'b' => s == "01234b" ? 4 : -1, 'c' => s == "01234c" ? 5 : -1, _ => -1, }, 8 => s == "aaaaaaaa" ? 6 : -1, _ => -1, };
これで、どの case にも当たらないときには「長さ比較 + 1文字比較」で終わり、
当たった時でもそれに加えて少数の文字列 == になります。
例えば .NET の中の人が HTTP の content type 用の分岐で測った感じだと、 5~10倍くらい速いみたいです。
