Sim*_*itt 20 c# optimization performance il postfix-notation
由于这个问题是关于增量运算符和带前缀/后缀表示法的速度差异,我将非常仔细地描述这个问题,以免Eric Lippert发现它并激怒我!
(有关我为什么要问的更多信息和详细信息,请访问http://www.codeproject.com/KB/cs/FastLessCSharpIteration.aspx?msg=3899456#xx3899456xx/)
我有四个代码片段如下: -
(1)单独,前缀:
for (var j = 0; j != jmax;) { total += intArray[j]; ++j; }
Run Code Online (Sandbox Code Playgroud)
(2)单独,后缀:
for (var j = 0; j != jmax;) { total += intArray[j]; j++; }
Run Code Online (Sandbox Code Playgroud)
(3)Indexer,Postfix:
for (var j = 0; j != jmax;) { total += intArray[j++]; }
Run Code Online (Sandbox Code Playgroud)
(4)索引器,前缀:
for (var j = -1; j != last;) { total += intArray[++j]; } // last = jmax - 1
Run Code Online (Sandbox Code Playgroud)
我试图做的是证明/反驳在这个上下文中前缀和后缀表示法之间是否存在性能差异(即局部变量因此不易变,不能从另一个线程等变化)如果存在,为什么会出现这种情况.
速度测试表明:
(1)和(2)以相同的速度运行.
(3)和(4)以相同的速度运行.
(3)/(4)比(1)/(2)慢〜27%.
因此,我得出的结论是,在postfix表示法本身上选择前缀表示法没有性能优势.但是,当实际使用操作的结果时,这会导致代码比简单地丢弃的代码慢.
然后,我使用Reflector查看生成的IL,发现以下内容:
在所有情况下,IL字节的数量是相同的.
.maxstack在4到6之间变化,但我认为它仅用于验证目的,因此与性能无关.
(1)和(2)产生完全相同的IL,因此时间相同并不奇怪.所以我们可以忽略(1).
(3)和(4)生成了非常相似的代码 - 唯一相关的区别是dup操作码的定位以考虑操作的结果.同样,时间相同也就不足为奇了.
所以我然后比较(2)和(3)找出可以解释速度差异的因素:
(2)使用ldloc.0 op两次(一次作为索引器的一部分,然后作为增量的一部分).
(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
Run Code Online (Sandbox Code Playgroud)
(3)看起来像这样:
ldloc.0
dup // j on the stack for the *Result of the Operation*
ldc.i4.1
add
stloc.0
Run Code Online (Sandbox Code Playgroud)
(4)看起来像这样:
ldloc.0
ldc.i4.1
add
dup // j + 1 on the stack for the *Result of the Operation*
stloc.0
Run Code Online (Sandbox Code Playgroud)
现在(终于!)问题:
是否(2)更快,因为JIT编译器识别一个ldloc.0/ldc.i4.1/add/stloc.0
简单地将局部变量递增1并优化它的模式?(并且dup
(3)和(4)中存在中断该模式,因此错过了优化)
补充说:如果这是真的那么,对于(3)至少,不会取代dup
另一个ldloc.0
重新引入那种模式?
Sim*_*itt 10
经过多次研究后确定(我很难过!),我想已经回答了我自己的问题:
答案是可能的.显然JIT编译器确实寻找模式(参见http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx)来决定什么时候以及如何优化数组边界检查但是它是否是我猜测的相同模式我不知道.
在这种情况下,这是一个有争议的问题,因为(2)的相对速度增加是由于更多的东西.事实证明,x64 JIT编译器非常聪明,可以确定数组长度是否为常量(并且看似也是循环中展开次数的倍数):所以代码只是在每次迭代结束时进行边界检查每个展开只是: -
total += intArray[j]; j++;
00000081 8B 44 0B 10 mov eax,dword ptr [rbx+rcx+10h]
00000085 03 F0 add esi,eax
Run Code Online (Sandbox Code Playgroud)
我通过更改应用程序以在命令行上指定数组大小并查看不同的汇编程序输出来证明这一点.
在此练习中发现的其他事项: -
有趣的结果.我会做的是:
然后你会知道抖动是否比其他抖动做得更好.例如,抖动可能意识到在一种情况下它可以删除数组边界检查,但在另一种情况下没有意识到.我不知道; 我不是抖动方面的专家.
所有rigamarole的原因是因为连接调试器时抖动可能会生成不同的代码.如果你想知道在正常情况下它做了什么,那么你必须确保代码在正常的非调试器环境下进行搜索.
我喜欢性能测试,我喜欢快速的程序,所以我很佩服你的问题.
我试图重现你的发现并失败了.在我的Intel i7 x64系统上,在x86 | Release配置中运行.NET4框架上的代码示例,所有四个测试用例产生大致相同的时序.
为了进行测试,我创建了一个全新的控制台应用程序项目,并使用QueryPerformanceCounter
API调用来获得基于CPU的高分辨率计时器.我试过两个设置jmax
:
jmax = 1000
jmax = 1000000
因为数组的位置通常会对性能的表现和循环的大小增加产生很大的影响.但是,在我的测试中,两个数组大小的行为都相同.
我已经做了很多性能优化,我学到的一件事就是你可以非常轻松地优化应用程序,以便它在一台特定的计算机上运行得更快,同时无意中导致它在另一台计算机上运行得更慢.
我不是假设在这里谈论.我已经调整了内部循环并花费了数小时和数天的工作来使程序运行得更快,只是为了让我的希望破灭,因为我在工作站上优化它并且目标计算机是英特尔处理器的不同型号.
所以这个故事的寓意是:
这就是为什么有些编译器为不同的处理器配备了特殊的优化开关,或者某些应用程序有不同的版本,即使一个版本可以轻松地在所有支持的硬
因此,如果您要进行这样的测试,您必须以与JIT编译器编写者相同的方式执行此操作:您必须在各种硬件上执行测试,然后选择混合,这是一种可以提供最佳效果的快乐介质在无处不在的硬件上的性能.