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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct S | |
{ | |
public void M() | |
{ | |
System.Console.WriteLine($"M()"); | |
} | |
} | |
public class C | |
{ | |
private readonly static S s = new S(); | |
static void Main() | |
{ | |
s.M(); | |
s.M(); | |
s.M(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct S | |
{ | |
public void M() | |
{ | |
System.Console.WriteLine($"M() : {counter++}"); | |
} | |
private int counter; | |
} | |
public class C | |
{ | |
private readonly static S s = new S(); | |
static void Main() | |
{ | |
s.M(); | |
s.M(); | |
s.M(); | |
} | |
} |
M() : 0 M() : 1 M() : 2you 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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//#define READONLY | |
#if !READONLY | |
public struct S | |
#else | |
public readonly struct S | |
#endif | |
{ | |
public void M() | |
{ | |
#if !READONLY | |
System.Console.WriteLine($"M() : {counter++}"); | |
#endif | |
} | |
#if !READONLY | |
private int counter; | |
#endif | |
} | |
public class C | |
{ | |
static void Main() | |
{ | |
S s = new S(); | |
Foo(in s); | |
} | |
static void Foo(in S s) | |
{ | |
s.M(); // this will cause the struct to be copied | |
S localS= default; | |
localS.M(); | |
ref readonly S l = ref GetS(ref localS); | |
l.M(); // this will cause the struct to be copied | |
} | |
static ref readonly S GetS(ref S s) | |
{ | |
return ref s; | |
} | |
} |
- < 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).
Last but not least, if you are interested in deep content about .Net I recommend you to follow this blog
Adriano
No comments:
Post a Comment