Oct 20, 2018

Avoiding hidden data copies in C#

Lire ce post en Francais

Leia este post em Português

Given the following code, Can you spot any issues? Hint: there's at least one, related to value types & performance!

Ok, lets make it a little bit more obvious by changing it to be a logical error instead of only a performance issue (the problem would be easly found if we had unit tests in place). Now what would be the expected output of the following code? 

If you answered:
M() : 0
M() : 1
M() : 2
you will be surprised to see:
M() : 0
M() : 0
M() : 0

and now it should be clear that something is not right.

The problem is that the field s has been declared as readonly which caused the compiler to make a defensive copy before each s.M() invocation (the compiler does not perform flow analysis to prove the invoked method will not violate the readonly invariant); this means that M() is increasing counter on a temporary value each time it is called leaving s.counter alone. 

Notice that in the first version of the code which don't rely on the struct to be mutable (which is good, since you should avoid mutable value types) it is not easy to realize something is not quite right. Even with the later version it is not clear why counter never gets incremented. 

To understand that lets look in at the IL for the Main() method (you can also use sharplab.io to play with the code) :
You can observe the copy happening at IL offsets IL_0001 (ldsfld valuetype S C::s) & IL_0006 (stloc.0) then again at offset IL_000f & IL_0014 and finally at offsets IL_001d & IL_0022. Notice that after copying s to the local variable at slot 0 (zero) the address of this local variable is loaded into the stack and  M() method is invoked.

Unfortunately C# language provide other opportunities to play tricks like this on us; I can think of at least 2 cases: in parameters and local ref readonly variables. See the example below:
So how can you avoid such hidden copies? The answer depends which version of C# you are targeting:
  • < 7.3: The only way to get rid of those copies (AFAIK) is to remove readonly modifier from the field (s in this example)
  • >= 7.3:  in this case you can declare your struct as readonly so the compiler will enforce this invariant (and emit an error if you try to mutate any of the struct's state).
One question that you may have now is: how bad are those  extra copy operations? IMO it depends on whether they affect your program performance (i) and/or behavior (ii) and your requirements. In the case that the behavior of the program is affected I think it is a consensus that those copies need to go whereas in the case in which performance is affected it is debatable whether one should invest time/effort to chase/fix those (of course, you always should have a performance goal). That being said, if you use Visual Studio for development you can install ErrorProne.NET which will pinpoint this type of issues.

Last but not least, if you are interested in deep content about .Net I recommend you to follow this blog

Adriano

No comments: