由于这个问题是关于增量运算符和前缀/后缀符号的速度差异,我将非常仔细地描述这个问题,以免 Eric Lippert 发现它并激怒我!
(有关我为什么要询问的更多信息和详细信息,请访问 http://www.codeproject.com/KB/cs/FastLessCSharpIteration.aspx?msg=3899456#xx3899456xx/)
我有四个代码片段如下: -
(1) 分开,前缀:
for (var j = 0; j != jmax;) { total += intArray[j]; ++j; }
(2) 分开,后缀:
for (var j = 0; j != jmax;) { total += intArray[j]; j++; }
(3) 索引器,后缀:
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) 比 (1)/(2) 慢约 27%。
因此,我得出的结论是,选择前缀表示法而不是后缀表示法本身并没有性能优势。但是,当实际使用操作的结果时,这会导致代码比简单地丢弃更慢。
然后我查看了使用 Reflector 生成的 IL 并发现以下内容:
IL 字节数在所有情况下都是相同的。
.maxstack 在 4 到 6 之间变化,但我认为这仅用于验证目的,因此与性能无关。
(1) 和 (2) 生成完全相同的 IL,因此时序相同也就不足为奇了。所以我们可以忽略(1)。
(3) 和 (4) 生成了非常相似的代码 - 唯一相关的区别是 dup 操作码的定位以说明操作的结果。同样,时间相同也就不足为奇了。
因此,我比较了 (2) 和 (3) 以找出可能导致速度差异的原因:
(2) 两次使用 ldloc.0 操作(一次作为索引器的一部分,然后作为增量的一部分)。
(3) 使用 ldloc.0 紧跟一个 dup op。
因此,对于 (1) (和 (2)) 递增 j 的相关 IL 是:
// 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 并对其进行优化?(并且dup
(3)和(4)中的存在打破了这种模式,因此错过了优化)
还有一个补充:如果这是真的,那么至少对于(3),不会dup
用另一个ldloc.0
重新引入该模式来替换 吗?