2

In 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.

(Quoted from Microsoft Docs)


For example:

  1. For i+=2;, i would be modified directly without any new objects being created.
  2. For i=i+2;, a copy of i would be created at first. The copied one would be modified and then be assigned back to i.
        i_copied = i;
        i_copied += 2;
        i = i_copied;

Without any optimizations from compiler, the second method will construct a useless instance, which degrades the performance.


In C#, operators like += are not permitted to be overloaded. And all simple types like int or double are declared as readonly struct (Does that mean all of structs in C# are immutable in truth?).

I wonder in C#, is there a certain expression to force object be modified (at least for simple types) directly, without any useless instances being created.

And also, is it possible the C#-compiler optimizes the expression x=x+y to x+=y as expected, if there's no side-effect from constructors and deconstructors.

Community
  • 1
  • 1
zjyhjqs
  • 609
  • 4
  • 11
  • "both simple types", I guess you mean "both overloading the + operator", there are 'simple' types that don't (bool for instance) – vc 74 May 24 '20 at 09:41
  • 1
    _"In C/C++, we know that"_ - no, we don't. That is nonsense. – H H May 24 '20 at 09:53
  • @HenkHolterman I was suspecting that there is something wrong with that statement as well... But I don't know enough C to judge. Thank you for pointing that out. It would be weird if a language does that, wouldn't it? – Sweeper May 24 '20 at 09:55
  • 1
    @Sweeper - optimizers are much better than suggested here. 1) you don't have to worry. 2) if you are interested, you will have to measure. In the target situation, the surronding code can also affect this. – H H May 24 '20 at 10:10
  • @vc 74 Simple Types are described here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types#simple-types – zjyhjqs May 24 '20 at 10:16
  • On *simple types*, that is about C#, not about .NET. In particular, if these were `decimal`, the compiler will actually emit virtual calls to the addition operator of the `decimal` struct. For the types that the MSIL OpCode `add` can... Well, add... see [OpCodes.Add Field](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.add?view=netcore-3.1). – Theraot May 24 '20 at 10:21
  • @Henk Holterman "with the exception that, when we use the compound assignment, the left-hand operand is evaluated only once. If we use an ordinary assignment, that operand is evaluated twice: once in the expression on the right-hand side and again as the operand on the left hand. " in C++ Primer. – zjyhjqs May 24 '20 at 10:24
  • Those are all _as if_ rules. The optimizer is smart enough to know that 'evaluating an int' is a NOP and it is free to skip that. – H H May 24 '20 at 10:28
  • 1
    On *as if* rules, I have an example on the .NET side: MSIL is an stack language, that does not have knowledge of registers, that does not mean when the code runs it will do all operations on the stack and not use registers. One thing are the semantics of language, and another what the compiler actually does. – Theraot May 24 '20 at 10:52
  • @zjyhjqs Indeed, and bool is part of this list but does not overload + – vc 74 May 24 '20 at 18:40

3 Answers3

5

C#

When you compile C# into a .NET assembly, the code is in MSIL (Microsoft Intermediate Language). This allows the code to be portable. The .NET Runtime will compile it JIT for execution.

MSIL is an stack language. It does not know details of the target hardware (such as how many registers does the CPU have). There is only one way to write that addition:

    ldloc.0
    ldloc.1
    add
    stloc.0

Load the first local in the stack, load the second, add※ them, set the first local from the stack.

※: add pops two elements from the stack, adds them, and pushes the result back into the stack.

Thus, both x=x+y and x+=y will yield the same code.


Of course, there are optimizations that happens after. The JIT compiler will convert that into actual machine code.

This is what I see with SharpLab:

mov ecx, [ebp-4]
add ecx, [ebp-8]
mov [ebp-4], ecx

So, we copy [ebp-4] into ecx, add [ebp-8] to it, and then copy ecx back to [ebp-4].

So... Is the register ecx a useless instance?


Well, that is SharpLab, and that is JIT. A different compiler could in theory convert the code into something different on a different platform.

You can compile .NET code AOT to a native image, which will be more aggressive with optimizations. Although, I do not see how you are going to improve upon a simple addition. Oh, I know, it might see that you do not use this value and remove it, or may see that you are always adding the same values and replace it with a constant.

It might be worth noting that modern .NET JIT is able to continue to optimize the code during execution (it will quickly make a poorly optimized native version of the code, and later - once it is ready - replace it with a better version). This decision comes from the fact that on a JIT runtime, the performance depends on both the time it takes to create the native code and the time that native code takes to run.


C++

Let us see what C++ does. This is what I see for both x = x + y and x += y using godbolt (default settings※):

    mov     eax, DWORD PTR [rbp-8]
    add     DWORD PTR [rbp-4], eax
    mov     eax, DWORD PTR [rbp-4]

The instructions mov, add, mov match the ones we got from SharpLab, with a different choice of registers.

※: 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

Adding the compiler option -O made the code go away. Which makes sense because I was not using it.

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • Thanks. I'm not familiar with machine code. I guess that `add [ebp-4], [ebp-8]` would be more efficient. (Yes I think the register `ecx` is useless, at least in this situation.) Is it because "MSIL is a stack language."? So it's impossible to predict the concrete machine code because 2 kinds of transformation are executed simultaneously. – zjyhjqs May 24 '20 at 13:40
  • @zjyhjqs I'm not up to date with CPU instructions, however as far as I know, there is no way to do that… Or at least, there is a large set of CPUs that can't do that. – Theraot May 24 '20 at 15:48
  • In the case of a vector array, `x[i] += 1` generates `ldelema, dup, ldind... stind`, whereas `x[i] = x[i] + 1` generates `ldelem ... stelem` and accesses the indexer variable twice – Charlieface Feb 16 '21 at 00:13
4

To answer such questions you can use SharpLab.

As you can see both the generated IL and the JITted code are the same in both cases.

György Kőszeg
  • 17,093
  • 6
  • 37
  • 65
  • 1
    +1, for sake of completion, you could add an `M3` method showing the jitted code for `i = j + 2` and highlight the optimization – vc 74 May 24 '20 at 09:48
  • I actually wanted to highlight that post-increment operator may behave differently if used in compound expressions but I didn't complicate the answer in the end. – György Kőszeg May 24 '20 at 09:50
2

It is strictly equivalent. The form x += y is syntactic sugar for x = x + y.

Samuel Vidal
  • 883
  • 5
  • 16
  • A temporary instance of `x+y` would be created, which is not existed in `x += y`, though they have the same result. – zjyhjqs May 24 '20 at 10:28
  • 1
    @zjyhjqs - A C# programmer doesn't know about 'temporary instances'. You are looking at this wrong. – H H May 24 '20 at 13:15
  • @Henk Holterman This may be a weird question for C# programmers, but the 'temporary instances' must be existed in solution. I just want to get a proper answer, though it might do tiny effects in reality. – zjyhjqs May 24 '20 at 13:47
  • The intermediate result will probably only exist in a register. The compiler is free to generate the same code for both. And then it executes on an x86 RISC/CISC pipeline. Your difference is not tiny, it is zero. – H H May 24 '20 at 14:14