为什么添加本地variables会使.NET代码变慢
为什么要评论这个for循环的前两行,并取消第二个42%加速的结果呢?
int count = 0; for (uint i = 0; i < 1000000000; ++i) { var isMultipleOf16 = i % 16 == 0; count += isMultipleOf16 ? 1 : 0; //count += i % 16 == 0 ? 1 : 0; }
时序的背后是非常不同的汇编代码:循环中的13个指令和7个指令。 该平台是运行.NET 4.0 x64的Windows 7。 代码优化已启用,testing应用程序在VS2010之外运行。 [ 更新: Repro项目 ,用于validation项目设置。]
消除中间布尔值是一个基本的优化,是我1980年代龙书中最简单的一种。 在生成CIL或JITing x64机器码时,优化是如何不被应用的?
有没有“真的编译器,我想你优化这个代码,请”切换? 虽然我同情过早的优化是类似于爱的情绪,我可以看到在试图分析一个复杂的algorithm,像这样的问题分散在整个例程的挫折感。 你会通过热点工作,但没有暗示更广泛的温暖的地区,可以大大改善手工调整我们通常从编译器认为是理所当然的。 我当然希望我在这里错过了一些东西。
更新:对于x86也会出现速度差异,但取决于方法是即时编译的顺序。 请参阅为什么JIT订单会影响性能?
汇编代码 (根据要求):
var isMultipleOf16 = i % 16 == 0; 00000037 mov eax,edx 00000039 and eax,0Fh 0000003c xor ecx,ecx 0000003e test eax,eax 00000040 sete cl count += isMultipleOf16 ? 1 : 0; 00000043 movzx eax,cl 00000046 test eax,eax 00000048 jne 0000000000000050 0000004a xor eax,eax 0000004c jmp 0000000000000055 0000004e xchg ax,ax 00000050 mov eax,1 00000055 lea r8d,[rbx+rax]
count += i % 16 == 0 ? 1 : 0; 00000037 mov eax,ecx 00000039 and eax,0Fh 0000003c je 0000000000000042 0000003e xor eax,eax 00000040 jmp 0000000000000047 00000042 mov eax,1 00000047 lea edx,[rbx+rax]
问题应该是“为什么我的机器上看到这样的差异?”。 我不能再现如此巨大的速度差异,并怀疑有一些特定于您的环境的东西。 很难分辨出它可能是什么。 可以是你之前设置的一些(编译器)选项,忘记了它们。
我已经创build了一个控制台应用程序,在发行模式(x86)下重build,并在VS之外运行。 结果几乎相同,两种方法的结果都是1.77秒。 这里是确切的代码:
static void Main(string[] args) { Stopwatch sw = new Stopwatch(); sw.Start(); int count = 0; for (uint i = 0; i < 1000000000; ++i) { // 1st method var isMultipleOf16 = i % 16 == 0; count += isMultipleOf16 ? 1 : 0; // 2nd method //count += i % 16 == 0 ? 1 : 0; } sw.Stop(); Console.WriteLine(string.Format("Ellapsed {0}, count {1}", sw.Elapsed, count)); Console.ReadKey(); }
请任何有5分钟的人复制代码,重build,在VS之外运行,并发表评论的结果这个答案。 我想避免说“它适用于我的机器”。
编辑
为了确保我已经创build了一个64位 Winforms应用程序,结果与问题类似 – 第一个方法比第二个方法(1.05秒) 慢 (1.57秒)。 我观察到的差异是33% – 仍然很多。 似乎在.NET4 64位JIT编译器中有一个错误。
我无法与.NET编译器或其优化,甚至是执行其优化。
但在这种特定情况下,如果编译器将该布尔variables折叠到实际语句中,并且您要尝试debugging此代码,则优化后的代码将与写入的代码不匹配。 您将无法单步执行isMulitpleOf16赋值并检查它的值。
这只是优化可能被closures的一个例子。 可能有其他人。 优化可能发生在代码的加载阶段,而不是来自CLR的代码生成阶段。
现代运行时间非常复杂,特别是如果您在运行时抛出JIT和dynamic优化。 我感激代码完成了它所说的有时。
我认为这与你的其他问题有关。 当我如下更改您的代码时,多行版本将胜出。
哎呀,只在x86上。 在x64上,多线是最慢的,有条件的打败他们。
class Program { static void Main() { ConditionalTest(); SingleLineTest(); MultiLineTest(); ConditionalTest(); SingleLineTest(); MultiLineTest(); ConditionalTest(); SingleLineTest(); MultiLineTest(); } public static void ConditionalTest() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); int count = 0; for (uint i = 0; i < 1000000000; ++i) { if (i % 16 == 0) ++count; } stopwatch.Stop(); Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds); } public static void SingleLineTest() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); int count = 0; for (uint i = 0; i < 1000000000; ++i) { count += i % 16 == 0 ? 1 : 0; } stopwatch.Stop(); Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds); } public static void MultiLineTest() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); int count = 0; for (uint i = 0; i < 1000000000; ++i) { var isMultipleOf16 = i % 16 == 0; count += isMultipleOf16 ? 1 : 0; } stopwatch.Stop(); Console.WriteLine("Multi-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds); } }
这是.NET Framework中的一个错误。
那么,我真的只是猜测,但我提交了一个关于Microsoft Connect的错误报告,看看他们说什么。 微软删除了这个报告之后,我在GitHub的roslyn项目上重新提交了这个报告。
更新:微软已经把这个问题转移到了coreclr项目。 从这个问题的评论看来,把它称为一个bug似乎有点强大; 这更多的是缺less优化。
我倾向于这样想:在编译器上工作的人每年只能做很多事情。 如果在那个时候他们可以实现lambda或者经典的优化,那么我会投票给lambda。 C#是一种在读取和写入代码方面效率较高的语言,而不是执行时间。
因此,团队专注于最大限度地提高读写效率的特性是合理的,而不是某个angular落案例(其中可能有数千个)的执行效率。
最初,我相信,这个想法是JITter会做所有的优化。 不幸的是,JITting需要花费大量的时间,任何先进的优化都会使其变得更糟。 所以这样做并不如人们所希望的那样好。
我发现在C#中编写真正快速的代码的一件事情是,在你提到的任何优化都会产生影响之前,你经常遇到严重的GC瓶颈。 就像分配数百万个对象一样。 C#在避免成本方面离你很远:您可以使用结构数组来代替,但是相比之下,生成的代码真的很难看。 我的观点是,关于C#和.NET的许多其他决定使得这样的特定的优化不像C ++编译器那样有价值。 哎呀,他们甚至放弃了NGEN中针对CPU的优化,为程序员(debugging器)效率提供交易性能。
说完这一切,我真的很喜欢 C#,它实际上使用了自20世纪90年代以来C ++所使用的优化。 只是不要以像async / await这样的function为代价。