Гледах някакъв код с огромен оператор switch и if-else оператор за всеки случай и моментално почувствах желание да оптимизирам. Както винаги трябва да прави един добър програмист, аз се заех да получа някои факти за трудни времена и започнах с три варианта:
Оригиналният код изглежда така:
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; }
Вторият вариант, преобразуван за използване на условния оператор:
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); }
Забележка: двата оператора за превключване имат абсолютно еднакви регистри и речниците имат еднакво количество знаци.
Очаквах, че 1) и 2) са донякъде подобни по отношение на производителността и че 3) ще бъде малко по-бавно.
За всеки метод, изпълняван два пъти по 10 000 000 итерации за загряване и след това време, за мое учудване получавам следните резултати:
- 0,0000166 милисекунди на повикване
- 0,0000779 милисекунди на повикване
- 0,0000413 милисекунди на повикване
Как е възможно това? Условният оператор е четири пъти по-бавен от изразите if-else и почти два пъти по-бавен от търсенето в речника. Пропускам ли нещо съществено тук или условният оператор е присъщо бавен?
Актуализация 1: Няколко думи за моя тестов сбруя. Изпълнявам следния (псевдо)код за всеки от горните варианти под Release компилиран .Net 3.5 проект във Visual Studio 2010. Оптимизацията на кода е включена и константите DEBUG/TRACE са изключени. Изпълнявам метода при измерване веднъж за загрявка, преди да направя измерване на времето. Методът run изпълни метода за голям брой итерации, като shift
беше зададен както на 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: Задълбочавайки се, погледнах IL, генериран за 1) и 2) и открих, че структурите на главния превключвател са идентични, както бих очаквал, но корпусите на корпуса имат леки разлики. Ето 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/else се разклонява, когатоshift
е false. - Докато 1) всъщност се компилира до няколко повече инструкции от 2), броят на инструкциите, изпълнени, когато
shift
е вярно или невярно, е равен за двете. - Подреждането на инструкциите за 1) е такова, че само един слот за стека е зает през цялото време, докато 2) винаги зарежда два.
Някое от тези наблюдения предполага ли, че условният оператор ще работи по-бавно? Има ли други странични ефекти, които влизат в действие?