Я смотрел на некоторый код с огромным оператором 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); }
Примечание: два оператора switch имеют одинаковые случаи, а словари имеют одинаковое количество символов.
Я ожидал, что 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: Копаем дальше, я посмотрел на ИЛ, сгенерированный для 1) и 2), и обнаружил, что структуры главного коммутатора идентичны, как и следовало ожидать, но тела корпуса имеют небольшие отличия, Вот IL, на которую я смотрю:
1) Если оператор 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, а если /else веткится, когдаshift
является ложным. - В то время как 1) фактически компилируется еще несколько инструкций, чем 2), количество команд, выполняемых, когда
shift
является либо истинным, либо ложным, равны для двух. - Порядок инструкций для 1) таков, что только один слот стека занят все время, а 2) всегда загружает два.
У любого из этих наблюдений подразумевается, что условный оператор будет работать медленнее? Есть ли другие побочные эффекты, которые вступают в игру?