Is there any performance difference between ++i and i++ in C#?
使用类似
1 | for(int i = 0; i < 10; i++) { ... } |
和
1 | for(int i = 0; i < 10; ++i) { ... } |
或者编译器是否能够以这样一种方式进行优化:在功能相同的情况下,它们同样快速?
编辑:这是因为我和一个同事讨论过它,而不是因为我认为它在任何实际意义上都是一个有用的优化。这主要是学术性的。
在这种情况下,生成的中间代码对于++i和i++没有区别。给定此程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Program { const int counter = 1024 * 1024; static void Main(string[] args) { for (int i = 0; i < counter; ++i) { Console.WriteLine(i); } for (int i = 0; i < counter; i++) { Console.WriteLine(i); } } } |
生成的IL代码对于两个循环都是相同的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | IL_0000: ldc.i4.0 IL_0001: stloc.0 // Start of first loop IL_0002: ldc.i4.0 IL_0003: stloc.0 IL_0004: br.s IL_0010 IL_0006: ldloc.0 IL_0007: call void [mscorlib]System.Console::WriteLine(int32) IL_000c: ldloc.0 IL_000d: ldc.i4.1 IL_000e: add IL_000f: stloc.0 IL_0010: ldloc.0 IL_0011: ldc.i4 0x100000 IL_0016: blt.s IL_0006 // Start of second loop IL_0018: ldc.i4.0 IL_0019: stloc.0 IL_001a: br.s IL_0026 IL_001c: ldloc.0 IL_001d: call void [mscorlib]System.Console::WriteLine(int32) IL_0022: ldloc.0 IL_0023: ldc.i4.1 IL_0024: add IL_0025: stloc.0 IL_0026: ldloc.0 IL_0027: ldc.i4 0x100000 IL_002c: blt.s IL_001c IL_002e: ret |
也就是说,JIT编译器可能(尽管可能性很小)在某些特定的上下文中进行一些优化,这些优化将有利于一个版本胜过另一个版本。但是,如果存在这样的优化,它可能只影响循环的最终(或者可能是第一次)迭代。
简而言之,在您所描述的循环结构中,控制变量的简单预增量或后增量在运行时没有区别。
如果你问这个问题,你是在试图解决错误的问题。
要问的第一个问题是"如何通过加快软件的运行速度来提高客户对我的软件的满意度?"答案几乎是"使用+I而不是I++",反之亦然。
从"硬件是便宜的,程序员是昂贵的"这篇恐怖文章中可以看出:
Rules of Optimization:
Rule 1: Don't do it.
Rule 2 (for experts only): Don't do it yet.
-- M.A. Jackson
我阅读规则2的意思是"首先编写干净、清晰的代码,以满足客户的需求,然后在速度太慢的地方加快速度"。
啊…再次开放。好啊.成交了。
ildasm是一个开始,但不是结束。关键是:JIT将为程序集代码生成什么?
这是你想做的。
取一些你想看的东西的样本。很明显,如果你愿意的话,你可以用挂钟给他们计时——但我想你想知道的不止这些。
这是不明显的。C编译器生成一些在很多情况下都是非最佳的MSIL序列。它调优的JIT可以处理这些和其他语言的怪癖。问题是:只有一些人注意到的"怪癖"被调优了。
您真的希望生成一个示例,让您的实现尝试一下,返回到main(或任何位置)、sleep()s或其他可以附加调试器的地方,然后再次运行这些例程。
您不希望在调试器下启动代码,否则JIT将生成未优化的代码—听起来您希望知道它在真实环境中的行为。JIT这样做是为了最大化调试信息和最小化"跳跃"的当前源位置。不要在调试器下启动性能评估。
好啊。因此,一旦代码运行一次(即:JIT已经为它生成了代码),那么就在睡眠期间(或任何时候)附加调试程序。然后查看为这两个例程生成的x86/x64。
我的直觉告诉我,如果你像你所描述的那样使用了++I/I++——即:在一个独立的表达式中,没有重复使用右值结果——就不会有什么区别。不过,去看看那些整洁的东西难道不是很有趣吗?:)
如JimMichel所示,编译器将为编写for循环的两种方法生成相同的msil。
但那就是问题所在:没有理由去推测JIT或执行速度测量。如果两行代码生成相同的msil,那么它们不仅执行相同的操作,而且实际上是相同的。
JIT不可能区分循环,因此生成的机器代码也必须是相同的。
伙计们,"答案"是C和C++的。
C是另一种动物。
使用ildasm查看编译的输出以验证是否存在msil差异。
有一个具体的代码和clr版本在脑海中吗?如果是这样的话,就基准它。如果没有,那就忘了它。微观优化,所有这些…此外,您甚至不能确定不同的clr版本会产生相同的结果。
除其他答案外,如果您的
1 2 3 4 5 6 | static void Main(string[] args) { var sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 2000000000; ++i) { } //int i = 0; //while (i < 2000000000){++i;} Console.WriteLine(sw.ElapsedMilliseconds); |
3次运行的平均值:对于i++:1307对于with++i:1314
当使用i++时:1261当使用++I:1276时
这是一个2.53千兆赫的赛扬d。每次迭代大约需要1.6个CPU周期。这意味着CPU每循环执行一条以上的指令,或者JIT编译器展开循环。每次迭代中,i++和++i之间的差异只有0.01个CPU周期,可能是后台操作系统服务造成的。
根据这个答案,i++使用一个CPU指令的次数超过了++i。但我不知道这是否会导致性能差异。
因为任何一个循环都可以很容易地重写为使用后增量或前增量,所以我猜想编译器总是使用更有效的版本。