在 C# 中,x+=y 和 x=x+y(x 和 y 都是简单类型)之间有什么性能差异?

zjy*_*qs 2 c# operator-keyword

在 C/C++ 中,

复合赋值运算符将简单赋值运算符与另一个二元运算符组合在一起。复合赋值运算符执行附加运算符指定的操作,然后将结果分配给左操作数。例如,复合赋值表达式,如

expression1 += expression2

可以理解为

expression1 = expression1 + expression2

然而,复合赋值表达式并不等同于扩展版本,因为复合赋值表达式只计算 expression1 一次,而扩展版本计算 expression1 两次:在加法运算和赋值运算中。

(引自Microsoft Docs


例如:

  1. 对于i+=2;,i将直接修改而不创建任何新对象。
  2. 对于i=i+2;i首先会创建一个副本。复制的一个将被修改,然后被分配回i.
        i_copied = i;
        i_copied += 2;
        i = i_copied;
Run Code Online (Sandbox Code Playgroud)

如果没有编译器的任何优化,第二种方法将构造一个无用的实例,从而降低性能。


在 C# 中,+=不允许重载类似运算符。并且所有像or一样的简单类型都声明为(这是否意味着 C# 中的所有结构实际上都是不可变的?)。intdoublereadonly struct

我不知道在C#,是有一定的表达力对象被修改(至少对于简单类型)直接,而不被创建的任何无用实例。

而且,是否有可能在C#-compiler优化表达x=x+yx+=y不如预期,如果有一个从构造函数和deconstructors无副作用。

The*_*aot 5


C#

将 C# 编译为 .NET 程序集时,代码采用 MSIL(Microsoft 中间语言)。这允许代码是可移植的。.NET 运行时将编译它 JIT 以执行。

MSIL 是一种堆栈语言。它不知道目标硬件的详细信息(例如 CPU 有多少个寄存器)。只有一种方法可以编写该加法:

    ldloc.0
    ldloc.1
    add
    stloc.0
Run Code Online (Sandbox Code Playgroud)

加载堆栈中的第一个本地,加载第二个,添加?他们,从堆栈中设置第一个本地。

?: add从堆栈中弹出两个元素,将它们相加,并将结果压回到堆栈中。

因此,两者x=x+yx+=y将产生相同的代码。


当然,之后会发生一些优化。JIT 编译器会将其转换为实际的机器代码。

这是我在SharpLab 中看到的:

mov ecx, [ebp-4]
add ecx, [ebp-8]
mov [ebp-4], ecx
Run Code Online (Sandbox Code Playgroud)

所以,我们复制[ebp-4]ecx,添加[ebp-8]到它,然后复制ecx[ebp-4]

那么......寄存器ecx是一个无用的实例吗?


嗯,那是 SharpLab,那是 JIT。理论上,不同的编译器可以将代码转换为不同平台上的不同内容。

您可以将 .NET 代码 AOT 编译为本机映像,这将更积极地进行优化。虽然,我不知道你将如何改进一个简单的添加。哦,我知道,它可能会看到您没有使用此值并将其删除,或者可能会看到您总是添加相同的值并用常量替换它。

可能值得注意的是,现代 .NET JIT 能够在执行期间继续优化代码(它会迅速生成代码的优化不佳的本机版本,稍后 - 一旦准备就绪 - 将其替换为更好的版本)。这个决定来自这样一个事实,即在 JIT 运行时上,性能取决于创建本机代码所需的时间和运行本机代码所需的时间。


C++

让我们看看 C++ 做了什么。这就是我看到的x = x + yx += y使用Godbolt(默认设置?)的情况:

    mov     eax, DWORD PTR [rbp-8]
    add     DWORD PTR [rbp-4], eax
    mov     eax, DWORD PTR [rbp-4]
Run Code Online (Sandbox Code Playgroud)

说明mov, add,mov符合我们从SharpLab得到的,具有不同的选择寄存器。

?: x86-64 gcc 9.3 -g -o /tmp/compiler-explorer-compiler2020424-22672-17cap6k.bjoj/output.s -masm=intel -S -fdiagnostics-color=always /tmp/compiler-explorer-compiler2020424-22672-17cap6k.bjoj/example.cpp

添加编译器选项-O使代码消失。这是有道理的,因为我没有使用它。