Я просматривал некоторый код с огромным оператором 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: Продолжая копаться, я посмотрел на 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, тогда как если / else разветвляется, когда shift
равно false.
- Хотя 1) на самом деле компилируется с несколькими инструкциями больше, чем 2), количество инструкций, выполняемых, когда
shift
равно true или false, равно двум.
- Порядок команд для 1) таков, что всегда занят только один слот стека, а 2) всегда загружает два.
Подразумевает ли какое-либо из этих наблюдений, что условный оператор будет работать медленнее? Есть ли другие побочные эффекты, которые вступают в игру?