Ich sah mir einen Code mit einer riesigen switch-Anweisung und einer if-else-Anweisung für jeden Fall an und verspürte sofort den Drang zu optimieren. Wie es sich für einen guten Entwickler gehört, machte ich mich auf den Weg, um ein paar harte Timing-Fakten zu erhalten und begann mit drei Varianten:
-
Der ursprüngliche Code sieht wie folgt aus:
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; }
-
Die zweite Variante ist die Verwendung des bedingten Operators:
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; }
-
Ein Twist, bei dem ein mit Schlüssel/Zeichen-Paaren vorgefülltes Wörterbuch verwendet wird:
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); }
Hinweis: Die beiden switch-Anweisungen haben genau die gleichen Fälle und die Wörterbücher haben die gleiche Anzahl von Zeichen.
Ich hatte erwartet, dass 1) und 2) eine ähnliche Leistung erbringen und dass 3) etwas langsamer ist.
Für jede Methode, die zweimal 10.000.000 Iterationen zum Aufwärmen durchläuft und dann gemessen wird, erhalte ich zu meinem Erstaunen die folgenden Ergebnisse:
- 0,0000166 Millisekunden pro Anruf
- 0,0000779 Millisekunden pro Anruf
- 0,0000413 Millisekunden pro Anruf
Wie kann das sein? Der Bedingungsoperator ist viermal langsamer als if-else-Anweisungen und fast zweimal langsamer als Wörterbuchabfragen. Übersehe ich hier etwas Wesentliches oder ist der Bedingungsoperator von Natur aus langsam?
Aktualisierung 1: Ein paar Worte zu meinem Test-Kabelbaum. Ich lasse den folgenden (Pseudo-)Code für jede der oben genannten Varianten unter einer Freigabe kompiliertes .Net 3.5 Projekt in Visual Studio 2010. Die Code-Optimierung ist eingeschaltet und die DEBUG/TRACE-Konstanten sind deaktiviert. Ich führe die zu messende Methode einmal zum Aufwärmen aus, bevor ich einen zeitgesteuerten Lauf durchführe. Die Run-Methode führte die Methode für eine große Anzahl von Iterationen aus, mit shift
sowohl auf "true" als auch auf "false" gesetzt und mit einem ausgewählten Satz von Eingabetasten:
Run(method);
var stopwatch = Stopwatch.StartNew();
Run(method);
stopwatch.Stop();
var measure = stopwatch.ElapsedMilliseconds / iterations;
Die Methode Run sieht folgendermaßen aus:
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);
}
Update 2: Ich habe mir die für 1) und 2) erstellten AWLs angesehen und festgestellt, dass die Hauptschalterstrukturen erwartungsgemäß identisch sind, die Gehäusekörper jedoch leichte Unterschiede aufweisen. Hier ist die IL, die ich mir ansehe:
1) If/else-Anweisung:
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) Der bedingte Operator:
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
Einige Beobachtungen:
- Der bedingte Operator verzweigt, wenn
shift
gleich wahr ist, während if/else-Verzweigungen, wennshift
falsch ist. - Während 1) tatsächlich zu ein paar mehr Anweisungen kompiliert wird als 2), ist die Anzahl der Anweisungen, die ausgeführt werden, wenn
shift
entweder wahr oder falsch ist, sind für beide gleich. - Die Befehlsreihenfolge für 1) ist so, dass immer nur ein Stack-Slot belegt ist, während 2) immer zwei lädt.
Bedeutet eine dieser Beobachtungen, dass der bedingte Operator langsamer arbeitet? Gibt es andere Nebeneffekte, die ins Spiel kommen?