关于运营商:C#中的++ i和i ++之间是否存在性能差异?

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的意思是"首先编写干净、清晰的代码,以满足客户的需求,然后在速度太慢的地方加快速度"。++ii++不太可能成为解决方案。


啊…再次开放。好啊.成交了。

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版本会产生相同的结果。


除其他答案外,如果您的i不是int,则可能存在差异。在C++中,如果它是一个类的对象,该类具有运算符EDCOX1、2和EDCOX1(3)被重载,那么它可以产生区别,并且可能产生副作用。在这种情况下,++i的性能应该更好(取决于实施情况)。


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。但我不知道这是否会导致性能差异。

因为任何一个循环都可以很容易地重写为使用后增量或前增量,所以我猜想编译器总是使用更有效的版本。