私は、それぞれの場合に巨大なswitchステートメントとif-elseステートメントを含むコードを見ていて、すぐに最適化の衝動を感じました。優れた開発者として、私は常にいくつかの難しいタイミングの事実を把握するために着手し、3つのバリエーションから始めるべきです。
元のコードは次のようになります。
public static bool SwitchIfElse(Key inKey, out char key, bool shift) { switch (inKey) { case Key.A: if (shift) { key = 'A'; } else { key = 'a'; } return true; case Key.B: if (shift) { key = 'B'; } else { key = 'b'; } return true; case Key.C: if (shift) { key = 'C'; } else { key = 'c'; } return true; ... case Key.Y: if (shift) { key = 'Y'; } else { key = 'y'; } return true; case Key.Z: if (shift) { key = 'Z'; } else { key = 'z'; } return true; ... //some more cases with special keys... } key = (char)0; return false; }
条件演算子を使用するように変換された2番目のバリアント:
public static bool SwitchConditionalOperator(Key inKey, out char key, bool shift) { switch (inKey) { case Key.A: key = shift ? 'A' : 'a'; return true; case Key.B: key = shift ? 'B' : 'b'; return true; case Key.C: key = shift ? 'C' : 'c'; return true; ... case Key.Y: key = shift ? 'Y' : 'y'; return true; case Key.Z: key = shift ? 'Z' : 'z'; return true; ... //some more cases with special keys... } key = (char)0; return false; }
キーと文字のペアが事前に入力された辞書を使用したひねり:
public static bool DictionaryLookup(Key inKey, out char key, bool shift) { key = '\0'; if (shift) return _upperKeys.TryGetValue(inKey, out key); else return _lowerKeys.TryGetValue(inKey, out key); }
注:2つのswitchステートメントの大文字と小文字はまったく同じであり、辞書の文字数は同じです。
1)と2)のパフォーマンスは多少似ており、3)は少し遅くなると予想していました。
ウォームアップのために10.000.000回の反復を2回実行してから時間を計った各メソッドについて、驚いたことに、次の結果が得られました。
- 呼び出しあたり0.0000166ミリ秒
- 呼び出しあたり0.0000779ミリ秒
- 呼び出しあたり0.0000413ミリ秒
どうすればいいの?条件演算子は、if-elseステートメントよりも4倍遅く、辞書検索よりもほぼ2倍遅くなります。ここで重要な何かが欠けていますか、それとも条件演算子は本質的に遅いですか?
アップデート1:私のテストハーネスについて一言。Visual Studio2010のリリースコンパイル済み.Net3.5プロジェクトで、上記の各バリアントに対して次の(擬似)コードを実行します。コードの最適化がオンになり、DEBUG/TRACE定数がオフになります。時間指定の実行を行う前に、ウォームアップのために測定中のメソッドを1回実行します。shift
runメソッドは、trueとfalseの両方に設定され、入力キーの選択セットを使用して、メソッドを多数の反復で実行しました。
Run(method);
var stopwatch = Stopwatch.StartNew();
Run(method);
stopwatch.Stop();
var measure = stopwatch.ElapsedMilliseconds / iterations;
Runメソッドは次のようになります。
for (int i = 0; i < iterations / 4; i++)
{
method(Key.Space, key, true);
method(Key.A, key, true);
method(Key.Space, key, false);
method(Key.A, key, false);
}
アップデート2:さらに掘り下げて、1)と2)で生成されたILを調べたところ、メインのスイッチ構造は予想どおりに同じであることがわかりましたが、ケース本体にはわずかな違いがあります。これが私が見ているILです:
1)if / elseステートメント:
L_0167: ldarg.2
L_0168: brfalse.s L_0170
L_016a: ldarg.1
L_016b: ldc.i4.s 0x42
L_016d: stind.i2
L_016e: br.s L_0174
L_0170: ldarg.1
L_0171: ldc.i4.s 0x62
L_0173: stind.i2
L_0174: ldc.i4.1
L_0175: ret
2)条件演算子:
L_0165: ldarg.1
L_0166: ldarg.2
L_0167: brtrue.s L_016d
L_0169: ldc.i4.s 0x62
L_016b: br.s L_016f
L_016d: ldc.i4.s 0x42
L_016f: stind.i2
L_0170: ldc.i4.1
L_0171: ret
いくつかの観察:
- 条件演算子は、
shift
trueに等しい場合は分岐し、if/elseshift
はfalseの場合に分岐します。 - 1)は実際には2)よりもいくつかの命令にコンパイルされます
shift
が、がtrueまたはfalseの場合に実行される命令の数は、2つで同じです。 - 1)の命令順序は、常に1つのスタックスロットのみが占有され、2)は常に2つをロードするようになっています。
これらの観察結果のいずれかは、条件演算子のパフォーマンスが遅くなることを意味しますか?関係する他の副作用はありますか?