Поскольку этот вопрос касается оператора инкремента и разностей скоростей с префиксом/постфиксной нотацией, я очень подробно опишу этот вопрос, чтобы Эрик Липперт не обнаружил это и не пламенил меня!
(дополнительную информацию и более подробную информацию о том, почему я спрашиваю, можно найти на http://www.codeproject.com/KB/cs/FastLessCSharpIteration.aspx?msg=3899456#xx3899456xx/)
У меня есть четыре фрагмента кода следующим образом: -
(1) Отдельный, префикс:
for (var j = 0; j != jmax;) { total += intArray[j]; ++j; }
(2) Отдельно, Postfix:
for (var j = 0; j != jmax;) { total += intArray[j]; j++; }
(3) Indexer, Postfix:
for (var j = 0; j != jmax;) { total += intArray[j++]; }
(4) Индексатор, префикс:
for (var j = -1; j != last;) { total += intArray[++j]; } // last = jmax - 1
То, что я пытался сделать, было доказать/опровергнуть, есть ли разница в производительности между префиксной и постфиксной нотацией в этом контексте (т.е. локальная переменная, поэтому она не изменчива, не может меняться с другого потока и т.д.), и если это было, почему это будет.
Тест скорости показал, что:
-
(1) и (2) работают с одинаковой скоростью, как и другие.
-
(3) и (4) работают с одинаковой скоростью, как и другие.
-
(3)/(4) на ~ 27% медленнее, чем (1)/(2).
Поэтому я заключу, что нет преимущества в производительности при выборе префиксной нотации поверх постфиксной записи как таковой. Однако, когда результат операции фактически используется, тогда это приводит к более медленному коду, чем если бы он просто был выброшен.
Затем я просмотрел сгенерированный IL с использованием Reflector и нашел следующее:
-
Количество байтов IL одинаково во всех случаях.
-
.Maxstack варьировался от 4 до 6, но я считаю, что он используется только для целей проверки и поэтому не имеет отношения к производительности.
-
(1) и (2) генерируют точно такой же IL, поэтому неудивительно, что время было идентичным. Поэтому мы можем игнорировать (1).
-
(3) и (4) генерируют очень похожий код - единственное релевантное различие заключается в позиционировании кода операции dup для учета результата операции. Опять же, не удивительно, что время идентично.
Итак, я сравнил (2) и (3), чтобы узнать, что могло бы объяснить разницу в скорости:
-
(2) дважды использует ldloc.0 op (один раз как часть индексатора, а затем позже как часть приращения).
-
(3) используется ldloc.0, за которым сразу следует дуп op.
Таким образом, соответствующий IL для приращения j для (1) (и (2)):
// ldloc.0 already used once for the indexer operation higher up
ldloc.0
ldc.i4.1
add
stloc.0
(3) выглядит следующим образом:
ldloc.0
dup // j on the stack for the *Result of the Operation*
ldc.i4.1
add
stloc.0
(4) выглядит следующим образом:
ldloc.0
ldc.i4.1
add
dup // j + 1 on the stack for the *Result of the Operation*
stloc.0
Теперь (наконец!) на вопрос:
Является ли (2) быстрее, потому что компилятор JIT распознает шаблон ldloc.0/ldc.i4.1/add/stloc.0
как просто увеличивая локальную переменную на 1 и оптимизируя ее?
(и наличие a dup
в (3) и (4) нарушает этот шаблон, и поэтому оптимизация пропущена)
И дополнительная:
Если это верно, то, по крайней мере, для (3) не заменил бы dup
другим ldloc.0
повторным введением этого шаблона?