在 C/C++ 中,
The compound-assignment operators combine the simple-assignment operator with another binary operator. Compound-assignment operators perform the operation specified by the additional operator, then assign the result to the left operand. For example, a compound-assignment expression such as
expression1 += expression2
can be understood as
expression1 = expression1 + expression2
However, the compound-assignment expression is not equivalent to the expanded version because the compound-assignment expression evaluates expression1 only once, while the expanded version evaluates expression1 twice: in the addition operation and in the assignment operation.
(引自 Microsoft Docs)
例如:
- 对于
i+=2;
,i
会被直接修改而不创建任何新对象。
- 对于
i=i+2;
,首先会创建i
的副本。复制的一个将被修改,然后被分配回i
。
i_copied = i;
i_copied += 2;
i = i_copied;
如果没有编译器的任何优化,第二种方法将构造一个无用的实例,这会降低性能。
在 C# 中,不允许重载像 +=
这样的运算符。和所有 simple types int
或 double
被声明为 readonly struct
(这是否意味着 C# 中的所有结构实际上都是不可变的?)。
我想知道在 C# 中,是否有某种表达式强制直接修改对象(至少对于简单类型),而不创建任何无用的实例。
此外,如果构造函数没有副作用,C# 编译器是否可以按预期将表达式 x=x+y
优化为 x+=y
和解构器。
C#
当您将 C# 编译成 .NET 程序集时,代码使用 MSIL(Microsoft 中间语言)。这允许代码是可移植的。 .NET 运行时将对其进行 JIT 编译以供执行。
MSIL 是一种堆栈语言。它不知道目标硬件的详细信息(例如 CPU 有多少个寄存器)。只有一种写法:
ldloc.0
ldloc.1
add
stloc.0
在堆栈中加载第一个局部变量,加载第二个局部变量,添加*它们,从堆栈中设置第一个局部变量。
※: add
从堆栈中弹出两个元素,将它们相加,并将结果推回堆栈。
因此,x=x+y
和 x+=y
将产生相同的代码。
当然,之后还有一些优化。 JIT 编译器会将其转换为实际的机器代码。
这是我用 SharpLab 看到的:
mov ecx, [ebp-4]
add ecx, [ebp-8]
mov [ebp-4], ecx
所以,我们将[ebp-4]
复制到ecx
中,添加[ebp-8]
,然后复制ecx
返回到 [ebp-4]
。
所以...寄存器 ecx
是一个无用的实例吗?
嗯,那就是 SharpLab,那就是 JIT。理论上,不同的编译器可以将代码转换为不同平台上的不同内容。
您可以将 .NET 代码 AOT 编译为 native 镜像,这将更加积极地进行优化。虽然,我看不到您将如何改进简单的添加。 哦,我知道,它可能会看到您没有使用此值并将其删除,或者可能会看到您总是添加相同的值并将其替换为常量。
可能值得注意的是,现代 .NET JIT 能够在执行期间继续优化代码(它会很快生成优化不佳的 native 代码版本,稍后 - 一旦准备就绪 - 将其替换为更好的代码版本)。这个决定来自这样一个事实,即在 JIT 运行时,性能取决于创建 native 代码所花费的时间和 native 代码运行所花费的时间。
C++
让我们看看 C++ 做了什么。这是我使用 godbolt 看到的 x = x + y
和 x += y
(默认设置※):
mov eax, DWORD PTR [rbp-8]
add DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
mov
、add
、mov
指令与我们从 SharpLab 获得的指令相匹配,但寄存器选择不同。
※: x86-64 gcc 9.3 with -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
使代码消失。这是有道理的,因为我没有使用它。