x=x+1 vs. x +=1

32

我的理解是这两个命令最终的结果相同,即将X增加1,但后者可能更有效率。

如果我理解有误,请解释一下它们的区别。

如果我理解正确,为什么后者更有效率?难道它们不应该编译成相同的IL代码吗?

谢谢。


6
别忘了老式的 ++x 和 x++。如果你只是单纯地要递增,我更喜欢使用它们。 - John Kraft
9
@John:这个问题标记为VB.NET。它没有这样的运算符。 - Mehrdad Afshari
2
Yishai - 那是一个不恰当的编辑。如果原帖发布者添加了vb.net标签,那就是他们感兴趣的,而不是你选择的其他随机语言。 - Alnitak
我认为最好使用你更容易阅读和理解其功能的内容。对于这些类型的事情,将“使其高效”部分留给编译器处理。 - Brian Kim
误解了Mehrdad的评论。感谢您将其恢复。 - Yishai
1
请记住编译器会为您的代码添加许多小的效率提升。使用Reflector查看编译器将如何调整这些操作以获得收益总是很有趣的。不要把这当作留下糟糕代码的借口,但有时可读性更好,而不是完全优化 - 而且通常净结果是相等的。 - STW
17个回答

111

MSDN +=库中得知:

使用此运算符几乎与指定结果 = result + expression相同,唯一的区别是result只被计算一次。

因此,它们并不完全相同,这就是为什么 x += 1 更有效率的原因。

更新:我刚刚注意到我的MSDN库链接指向JScript页面而不是VB页面,后者不包含相同的引用。

因此,经过进一步的研究和测试,该答案不适用于VB.NET。我错了。这里是一个示例控制台应用程序:

Module Module1

Sub Main()
    Dim x = 0
    Console.WriteLine(PlusEqual1(x))
    Console.WriteLine(Add1(x))
    Console.WriteLine(PlusEqual2(x))
    Console.WriteLine(Add2(x))
    Console.ReadLine()
End Sub

Public Function PlusEqual1(ByVal x As Integer) As Integer
    x += 1
    Return x
End Function

Public Function Add1(ByVal x As Integer) As Integer
    x = x + 1
    Return x
End Function

Public Function PlusEqual2(ByVal x As Integer) As Integer
    x += 2
    Return x
End Function

Public Function Add2(ByVal x As Integer) As Integer
    x = x + 2
    Return x
End Function

End Module

对于PlusEqual1和Add1来说,它们的IL确实是相同的:

.method public static int32 Add1(int32 x) cil managed
{
.maxstack 2
.locals init (
    [0] int32 Add1)
L_0000: nop 
L_0001: ldarg.0 
L_0002: ldc.i4.1 
L_0003: add.ovf 
L_0004: starg.s x
L_0006: ldarg.0 
L_0007: stloc.0 
L_0008: br.s L_000a
L_000a: ldloc.0 
L_000b: ret 
}

对于PlusEqual2和Add2,它们的IL几乎完全相同:

.method public static int32 Add2(int32 x) cil managed
{ 
.maxstack 2
.locals init (
    [0] int32 Add2)
L_0000: nop 
L_0001: ldarg.0 
L_0002: ldc.i4.2 
L_0003: add.ovf 
L_0004: starg.s x
L_0006: ldarg.0 
L_0007: stloc.0 
L_0008: br.s L_000a
L_000a: ldloc.0 
L_000b: ret 
}

23
令人印象深刻……实际的研究而非仅仅是猜测 :) +1 - Andrew Rollings
我很难相信编译器不会将其优化为inc [x]。这是一个简单的窥视孔优化。有人能够进行基准测试以找出吗? - Unknown
创建了一个应用程序并进行了反汇编。在IL中,这些函数是相同的。如果为每个操作生成的IL相同,那么文档怎么可能是正确的呢? - Gary.Ray
所以经过所有的麻烦,我是正确的 :)我赞赏你出色的侦探工作和努力。如果可以的话,我会给予超过+1的评价。 - z -
这只是一种假设,但我猜编译器进行了优化,使它变得相同,也许在像C这样的低级语言中,+=会更有效率。 - eran otzap
显示剩余2条评论

26

我写了一个简单的控制台应用:

static void Main(string[] args)
{
    int i = 0;
    i += 1;
    i = i + 1;
    Console.WriteLine(i);
}

我使用 Reflector 进行了反汇编,以下是我得到的内容:

private static void Main(string[] args)
{
    int i = 0;
    i++;
    i++;
    Console.WriteLine(i);
}

它们是相同的。


4
所以,例子中是每次加1,但我认为问题更多地涉及操作符的效率。如果使用 i += 2 和 i = i + 2,代码会如何反汇编? - CoderDennis
3
正如 @Mehrdad 上面所指出的,该问题标记为 vb.net,因此 i++ 不存在。我很好奇你的代码在 vb 版本中反汇编的结果是什么。现在前往下载 http://www.red-gate.com/products/reflector 。 - CoderDennis
球体和圆柱体的影子都可以是圆形。这并不是因为它恰好编译成相同的代码而语义上相等。 - xtofl
从Java语言规范来看,差异在于x+1被评估的次数:“形式为E1 op= E2的复合赋值表达式等同于E1 = (T)((E1) op (E2)),其中T是E1的类型,除了E1只会被评估一次。请注意,隐含的强制转换到类型T。”这证明了您在Java方面的说法是错误的。 - xtofl

18

它们编译成相同的结果,第二种方式只是更容易打出来。


4
请参考 Dennis Palmer 的回答,根据 MSDN 的信息它们不会编译成相同的结果,因此这个回答是不正确的。 - Steve Mitcham
Dennis Palmer的答案更准确:x只会被评估一次。因此,如果您有x.Member.Property(index).Field += y,则在获取旧值和设置新值之间存储+=前面的整个内容。 - Christian Klauser
@SealedSun:问题确实指定了 X += 1。不是 X() += 1 或者 X.Y() += 1。因此,您可以假设 X 是一个属性或变量。在此假设下,您能否给出一个它们不相等的例子? - Mehrdad Afshari
2
原来@yx是正确的。我被MSDN库搞混了,看到了JScript的+=描述。我更新了我的答案,提供了正确的链接和一个示例应用程序。 - CoderDennis

11

重要提示:

对于一般的语言来说,指定评估的答案在解释 += 的作用方面是正确的。但在VB.NET中,我假设OP中指定的X是一个变量或属性。


它们很可能编译成相同的IL代码。

更新(以解决可能存在的争议):

VB.NET是一种编程语言的规范。任何符合规范定义的编译器都可以成为VB.NET实现。如果您编辑MS VB.NET编译器的源代码以生成X += 1的低效代码,您仍将符合VB.NET规范(因为它没有说明它将如何工作,只是说效果将完全相同,这使得生成相同的代码是合理的)。

虽然编译器非常非常可能(我确实这样认为)为两者生成相同的代码,但它是相当复杂的软件。事实上,即使编译相同的代码两次,你也不能保证编译器会生成完全相同的代码!

您可以100%放心地说(除非您非常熟悉编译器的源代码),好的编译器应该生成性能相同的代码,这可能是完全相同的代码,也可能不是。


7
谁知道呢?规范只保证它们执行相同的操作,但不能保证它们生成相同的中间语言(IL)。技术上讲,可能会构建一个编译器,对于这两个输入不生成完全相同的IL。这就是我为什么用“可能”来描述的原因。 - Mehrdad Afshari
2
永远不要对生成优化代码的编译器过于自信。虽然它很可能将其翻译成相同的内容,但它可能会在编译器中触发不同的代码路径,并且可能最终更改结果,以便它不会完全相同。 - Mehrdad Afshari
2
@Mehrdad - 说得好,关于不同编译器实现的观点我没有想到。将你的解释中的一些或全部内容移动到你的答案中可能是值得的,因为这是一个有趣的话题。 - Andrew Hare

8

有太多的猜测了!即使是关于反射器的结论也未必正确,因为它可以在反汇编时进行优化。

那么为什么你们中没有人看一下IL代码呢?看看下面的C#程序:

static void Main(string[] args)
{
    int x = 2;
    int y = 3;
    x += 1;
    y = y + 1;
    Console.WriteLine(x);
    Console.WriteLine(y);
}

这段代码会编译成以下IL代码:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 25 (0x19)
.maxstack 2
.locals init ([0] int32 x,
[1] int32 y)
// 这里省略了一些指令

IL_0004: ldloc.0
IL_0005: ldc.i4.1
IL_0006: add
IL_0007: stloc.0

IL_0008: ldloc.1
IL_0009: ldc.i4.1
IL_000a: add
IL_000b: stloc.1

// 这里省略了一些指令
}

可以看到,实际上是完全一样的。因为IL的作用是告诉计算机要做什么,而不是怎么做。优化的工作将由JIT编译器完成。顺便提一下,VB.Net也是一样的。


1
“IL 的目的是告诉要做什么,而不是如何做”和“优化将由 JIT 编译器完成”的说法都是错误的。高级语言编译器也会进行优化,并且 IL 可以被认为是低级的,它讲述的是如何执行而不是为什么要执行。 - Mehrdad Afshari

4

在x86平台上,如果寄存器eax保存了变量x,那么下面两行代码都会产生类似的结果:

inc eax;

所以你是对的,在某些编译阶段后,中间语言(IL)将是相同的。

还有一系列类似的问题可以用"信任你的优化器"来回答。

一个著名的谬论是:
x++;

++x;
效率低,因为它需要存储一个临时值。但是如果你从不使用这个临时值,优化器会删除该存储操作。


3
我曾经为了好玩而专门编写了一个编译器,可以在x++上执行89个额外的操作。 - Joe Phillips
我以为是Jon Skeet写的 :) - Eduardo Molteni
1
如果x是C++的迭代器,那么++x更有效率。 - Milan Babuškov
如果你所说的“IS”是指“如果编译器未内联方法且消除不必要的工作,则可能会发生”。现代优化器进行整个程序优化,甚至跨模块进行内联,因此需要考虑此类情况的范围正在逐渐减少。 - Drew Hoskins

2

这两个内容是相同的。

x=x+1 

数学被视为一种矛盾的存在。
x+=1

不是和是轻松打字。


有趣的一点是:函数式语言(更接近数学)不会有复合赋值! - xtofl

2

在VB中它们可能是相同的;但在C语言中它们不一定相同(因为该操作符起源于C语言)。


2
这个操作码是为什么许多编程语言具有++运算符的原因。它生成的IL并不像JIT创建的本机代码那样重要。 - Matthew Whited

2
  1. 是的,它们的行为相同。
  2. 不,它们可能同样有效。优化器擅长这种事情。如果你想再次确认,请编写优化后的代码并在反编译器中查看。

2

如果x是一个简单类型,如int或float,则优化器可能会产生相同的结果。

如果您使用了其他语言(对VB知识有限,您可以重载+=吗?),其中x可以是一个大型对象,前者将创建一个额外的副本,可能会占用数百兆字节。后者则不会。


在任何现代编译器中,优化器都会使它们相同。在某些旧的编译器(不进行优化的编译器!)中,它们会有所不同,但效果相同(增加x)。 - Michael Kohne
我总是在优化器周围使用“可能”这个词,因为一个人不应该假设任何东西。不过,如果x=x+1,其中x是int类型的变量,是你最大的性能问题,我想你会非常独特。 :) - Macke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接