1. ホーム
  2. c#

switch on string と elseif on type はどちらが速いか?

2023-07-17 19:31:38

質問

文字列の比較に基づいて取るべきコードパスを特定するオプションがあるとしましょうか、さもなければ型をiffingします。

どちらがより速いですか、そしてそれはなぜですか?

switch(childNode.Name)
{
    case "Bob":
      break;
    case "Jill":
      break;
    case "Marko":
      break;
}

if(childNode is Bob)
{
}
elseif(childNode is Jill)
{
}
else if(childNode is Marko)
{
}

更新しました。 私がこれを尋ねる主な理由は、switch文がケースとしてカウントされるものについて独特であるためです。たとえば、それは変数の使用を許可しませんが、メイン アセンブリに移動される定数だけは許可します。私は、switch文がおかしなことをするために、このような制限があるのだと思いました。もしそれが elseif にのみ変換されるのであれば (ある投稿者がコメントしたように)、なぜ case 文で変数を許可されないのでしょうか?


洞窟。 後最適化をしています。このメソッドは 多く 回呼び出されます。

どのように解決するのですか?

Greg のプロファイルの結果は、彼がカバーした正確なシナリオでは素晴らしいものですが、興味深いことに、比較するタイプの数、相対的な頻度、基礎となるデータのパターンなど、多くの異なる要因を考慮すると、異なる手法の相対コストは劇的に変化します。

正確な答えを得るには、自分のシステムでさまざまな方法でパフォーマンスを測定する必要があります。

If/Else チェーンは、少数の型比較のための効果的なアプローチであり、または、どの少数の型が表示されるものの大部分を構成することになるかを確実に予測できる場合です。 このアプローチの潜在的な問題は、型の数が増加すると、実行しなければならない比較の数も増加することです。

を実行した場合、次のようになります。

int value = 25124;
if(value == 0) ...
else if (value == 1) ...
else if (value == 2) ...
...
else if (value == 25124) ... 

は、正しいブロックに入る前に、前の各if条件が評価されなければなりません。 一方

switch(value) {
 case 0:...break;
 case 1:...break;
 case 2:...break;
 ...
 case 25124:...break;
}

は、正しいコードのビットへの単純なジャンプを一つ実行します。

この例でより複雑になるのは、他のメソッドが整数値ではなく文字列のスイッチを使用している点で、これは少し複雑になっています。 低レベルでは、文字列は整数値と同じ方法で切り替えることができないので、C#コンパイラーはこれを動作させるためにいくつかのマジックを行います。

switchステートメントが十分に小さい場合(コンパイラが自動的に最適と考えることを行う場合)、文字列をオンにすると、if/elseチェーンと同じコードが生成されます。

switch(someString) {
    case "Foo": DoFoo(); break;
    case "Bar": DoBar(); break;
    default: DoOther; break;
}

とは同じです。

if(someString == "Foo") {
    DoFoo();
} else if(someString == "Bar") {
    DoBar();
} else {
    DoOther();
}

辞書内のアイテムのリストが十分に大きくなると、コンパイラは自動的に内部辞書を作成し、スイッチ内の文字列から整数のインデックスにマップし、そのインデックスに基づいてスイッチを作成します。

次のようになります (私がわざわざ入力するよりも多くのエントリを想像してみてください)。

静的フィールドは、型の switch 文を含むクラスに関連付けられた "hidden" 場所に定義されます。 Dictionary<string, int> であり、揶揄されるような名前が付けられています。

//Make sure the dictionary is loaded
if(theDictionary == null) { 
    //This is simplified for clarity, the actual implementation is more complex 
    // in order to ensure thread safety
    theDictionary = new Dictionary<string,int>();
    theDictionary["Foo"] = 0;
    theDictionary["Bar"] = 1;
}

int switchIndex;
if(theDictionary.TryGetValue(someString, out switchIndex)) {
    switch(switchIndex) {
    case 0: DoFoo(); break;
    case 1: DoBar(); break;
    }
} else {
    DoOther();
}

いくつかの簡単なテストを実行したところ、If/Else メソッドは 3 つの異なる型 (型はランダムに分布) で switch よりも約 3 倍高速でした。 25 のタイプでは、スイッチはわずかなマージン (16%) で速く、50 のタイプではスイッチは 2 倍以上速くなります。

もし、大量のタイプでスイッチするのであれば、私は3番目の方法をお勧めします。

private delegate void NodeHandler(ChildNode node);

static Dictionary<RuntimeTypeHandle, NodeHandler> TypeHandleSwitcher = CreateSwitcher();

private static Dictionary<RuntimeTypeHandle, NodeHandler> CreateSwitcher()
{
    var ret = new Dictionary<RuntimeTypeHandle, NodeHandler>();

    ret[typeof(Bob).TypeHandle] = HandleBob;
    ret[typeof(Jill).TypeHandle] = HandleJill;
    ret[typeof(Marko).TypeHandle] = HandleMarko;

    return ret;
}

void HandleChildNode(ChildNode node)
{
    NodeHandler handler;
    if (TaskHandleSwitcher.TryGetValue(Type.GetRuntimeType(node), out handler))
    {
        handler(node);
    }
    else
    {
        //Unexpected type...
    }
}

これはTed Elliotが提案したものと似ていますが、完全な型オブジェクトの代わりにランタイムの型ハンドルを使うことで、リフレクションを通して型オブジェクトをロードするオーバーヘッドを回避しています。

以下は私のマシンでのいくつかの簡単なタイミングです。

5,000,000のデータ要素(mode=Random)と5つのタイプで3つの反復処理をテストしました。
方法 時間 最適の割合
If/Else 179.67 100.00
TypeHandleDictionary 321.33 178.85
TypeDictionary 377.67 210.20
スイッチ 492.67 274.21

5,000,000 個のデータ要素(mode=Random)と 10 種類のタイプで 3 回の繰り返しテスト
方法 時間 最適の割合
If/Else 271.33 100.00
271.33 100.00 TypeHandleDictionary 312.00 114.99
312.00 114.99 TypeDictionary 374.33 137.96
スイッチ 490.33 180.71

5,000,000 個のデータ要素(mode=Random)と 15 種類の型による 3 回の繰り返しテスト
メソッド 時間 最適に対する割合
TypeHandleDictionary 312.00 100.00
312.00 100.00 If/Else 369.00 118.27
TypeDictionary 371.67 119.12
スイッチ 491.67 157.59

5,000,000 個のデータ要素(mode=Random)と 20 種類の型による 3 回の繰り返しテスト
メソッド 時間 最適に対する割合
TypeHandleDictionary 335.33 100.00
TypeDictionary 373.00 111.23
If/Else 462.67 137.97
スイッチ 490.33 146.22

5,000,000 個のデータ要素(mode=Random)と 25 種類の型による 3 回の繰り返しテスト
メソッド 時間 最適に対する割合
TypeHandleDictionary 319.33 100.00
319.33 100.00 TypeDictionary 371.00 116.18
スイッチ 483.00 151.25
483.00 151.25 Switch If/Else 562.00 175.99

5,000,000 個のデータ要素 (mode=Random) と 50 種類の型による 3 回の繰り返しテスト
メソッド 時間 最適に対する割合
TypeHandleDictionary 319.67 100.00
319.67 100.00 TypeDictionary 376.67 117.83
スイッチ 453.33 141.81
If/Else 1,032.67 323.04



少なくとも私のマシンでは、型ハンドル辞書のアプローチは、メソッドの入力に使用される型の分布がランダムである場合、15以上の異なる型に対して他のすべてのアプローチに勝ります。 の分布がランダムである場合、15 種類以上の型に対して他のすべての方法を上回ります。

一方、入力が if/else チェーンで最初にチェックされる型のみで構成されている場合、そのメソッドは 多く より高速になります。

5,000,000個のデータ要素(mode=UniformFirst)と50種類の型による3回の繰り返しテスト
方法 時間 最適の割合
If/Else 39.00 100.00
TypeHandleDictionary 317.33 813.68
317.33 813.68 TypeDictionary 396.00 1,015.38
スイッチ 403.00 1,033.33

逆に、入力が常にif/elseチェーンの最後のものであれば、逆効果になります。

5,000,000個のデータ要素(mode=UniformLast)と50種類のタイプで3回の繰り返しテストを行った結果
方法 時間 最適の割合
TypeHandleDictionary 317.67 100.00
スイッチ 354.33 111.54
TypeDictionary 377.67 118.89
If/Else 1,907.67 600.52

入力についていくつかの仮定ができる場合、最も一般的ないくつかの型について if/else チェックを行い、それが失敗した場合に辞書駆動アプローチにフォールバックするハイブリッドなアプローチで最高のパフォーマンスを得られるかもしれません。