MetallicPriest
MetallicPriest

Reputation: 30765

How does C# handle operator overloading

Say I have a complex numbers class in C#, whose addition operator is defined like this.

public static Complex operator +(Complex c1, Complex c2) 
 {
      return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
 }

and used like this.

 Complex c1, c2, c3, c4;

 c1 = new Complex(...)
 ...
 c4 = new Complex(...)

 Complex csum = c1 + c2 + c3 + c4;

Now, my question is how the C# compiler+runtime will handle this. Apparently, it looks like it will do something like this.

 ct1 = c1 + c2; // ct1 is a temporary object created by the compiler
 ct2 = ct1 + c3;
 csum = ct2 + c4;

Or is it smart enough to realize it can do it in a better way (less creation of new temporary objects) like this.

 ct = c1 + c2;
 ct += c3;
 csum = ct + c4; 

Upvotes: 3

Views: 317

Answers (2)

Joshua Honig
Joshua Honig

Reputation: 13215

Neither of your inferred stepwise procedures are correct with regards to the compiled output. The CLR is a stack machine, and since the code for adding all four at once does not indicate a need to separately allocate the intermediate totals as named variables it updates the running total in place (or rather pops and pushes the top stack element).

Both of your stepwise versions require the same number of actual operations; the unary addition version just requires one less local variable allocation.

Below is the actual IL produced by three different ways of adding the four input complex numbers. Note in my version I changed the Complex class (which I assume you lifted from How to: Use Operator Overloading to Create a Complex Number Class (C# Programming Guide)) to hold two double values instead of two ints. I have omitted the declaration of c1..c4 in both the C# and IL code for brevity. In short, both of the inferred stepwise approaches require two extra stloc.s (push a value to the variable list) and ldloc.s (retrieve a value from the variable list) calls each.

Original: Add all four in a row:

C#:

Complex csum = c1 + c2 + c3 + c4;

IL:

// MultipleAdd : 5 Locals, maxstack 3
IL_0068:  nop
IL_0069:  ldloc.0
IL_006a:  ldloc.1
IL_006b:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0070:  ldloc.2
IL_0071:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0076:  ldloc.3
IL_0077:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_007c:  stloc.s    csum
IL_007e:  ret

Inference 1: Three binary additions

C#:

Complex ct1 = c1 + c2; 
Complex ct2 = ct1 + c3;  
Complex csum = ct2 + c4; 

IL:

// BinaryAdd : 7 Locals, maxstack 3
IL_0068:  nop
IL_0069:  ldloc.0
IL_006a:  ldloc.1
IL_006b:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0070:  stloc.s    ct1
IL_0072:  ldloc.s    ct1
IL_0074:  ldloc.2
IL_0075:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_007a:  stloc.s    ct2
IL_007c:  ldloc.s    ct2
IL_007e:  ldloc.3
IL_007f:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0084:  stloc.s    csum
IL_0086:  ret 

Inference 2: Unary additions

C#:

Complex ct = c1 + c2; 
ct += c3; 
Complex csum = ct + c4;

IL:

// UnaryAdd : 6 Locals, maxstack 3
IL_0068:  nop
IL_0069:  ldloc.0
IL_006a:  ldloc.1
IL_006b:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0070:  stloc.s    ct
IL_0072:  ldloc.s    ct
IL_0074:  ldloc.2
IL_0075:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_007a:  stloc.s    ct
IL_007c:  ldloc.s    ct
IL_007e:  ldloc.3
IL_007f:  call       valuetype Test.Complex Test.Complex::op_Addition(valuetype Test.Complex,
                                                                    valuetype Test.Complex)
IL_0084:  stloc.s    csum
IL_0086:  ret

Upvotes: 1

Jon Skeet
Jon Skeet

Reputation: 1500345

The single expression is equivalent (in terms of generated code) to the separate statements, aside from there being a named local variable for it. The JIT may optimize that, but the compiler can't. Note that if Complex is a value type then it's not really going to involve allocating objects1.

It's worth noting that the + operator is handled specially for string in C# (it's specified by the language; it's not in the framework itself) precisely to avoid this sort of thing: x + y + z is (or at least can be) converted into string.Concat(x, y, z) to avoid creating temporary strings.


1 By "object" I mean an area of memory with an object header (type reference, sync block etc) followed by the fields, allocated on the heap, as opposed to a "value type value" which only includes the data itself, and may be on the heap or the stack. The stack/heap part is an implementation detail of course, but a potentially important one... and I believe it is reasonable to differentiate between "just a value type value" and "a full object".

Upvotes: 1

Related Questions