Fire up your favorite search engine, type in “mutable value types” and you might just feel a bit of pity for the poor little guys. It seems like everyone hates them. Truth be told, there’s a lot to dislike about them but before we get into the nastiness of mutable value types, let’s talk about why value types in general are oft-desirable.
Value types are the Oscar De Lay Hoyas of the CLR’s type system: they’re extremely fast and light. While heap allocation in the CLR is already quite fast, it does incur some overhead compared to stack-allocated objects. Stack-allocated objects (and value types embedded in heap-allocated objects) don’t need the garbage collector for reclamation and they don’t need the object pointer and sync block index that every heap-allocated object has. A quick and dirty test shows that allocating stack-based objects in a loop can be an order of magnitude faster than heap-based objects. This speedup not only reflects that there’s less work to allocate on the stack but that the CLR does some optimizations when using value types. Regardless of trickery, it’s a valid scenario and the value remains: value types can be really really fast.
This is why some, though few, types in Parallel Extensions are value types, even though they don’t really carry value semantics. SpinLock and SpinWait are both mutable types that were born strictly in the service of performance – every millisecond we can shave off of their allocation time can result in overall application performance improvements. There are problems with value types (that I’ll get to in a moment) and the Parallel Extension team spent quite a bit of time grappling with the tradeoffs between performance and usability. Ultimately, we decided that it’s acceptable for advanced types that exist purely for performance to eschew some usability for speed.
So what’s the bad stuff? Well, value types come with value semantics, and that means that any time you pass one around you’re transferring the value of the object, not the object itself. If a value type is assigned to another variable, whether that’s via the assignment of a local variable or as a parameter to a method, the receptive variable is getting assigned a copy of the target object. Add a readonly modifier to a value-type field in C# and any time you even access that field, you’re getting a copy. For mutable value-types, the danger is clear: once a value-type has been copied, mutations will reflect only in the copy.
This is really bad news for something like a SpinLock where a single instance must be shared between multiple threads to work properly. Consider the following C# example, where we use a collection of SpinLocks to protect partitions of data:
List<SpinLock> locks = InitializeLocks(); ... // on some number of threads SpinLock partitionLock = locks[myIndex]; // BUG! bool lockIsTaken = false; try { partitionLock.Enter(ref lockIsTaken); UpdatePartition(myIndex); } finally { if (lockIsTaken) partitionLock.Exit(); }
The seemingly innocuous storage of locks in a List gives us one gnarly concurrency bug. Because SpinLock is a value type, when we retrieve a partition lock via List’s indexer, we actually get a new copy of a SpinLock and our critical section no longer executes with mutual exclusion. Extension methods, which take the this-parameter by value, suffer from the same danger.
So take heed of this issue. In general, don’t use the value types unless you’re sure it’s going to give you the performance increases you need. When you have to use them, avoid passing them around and do so by reference, if you must. Document the dangers in your source. Finally, never, ever, put a readonly modifier on a SpinLock or SpinWait field.
Alternatively, if you want the functionality of SpinLock but need to pass it around and don’t mind the extra perf hit during allocation, you could always write a SpinLock wrapper class that can safely be passed around. Here’s a very simple version:
class SafeSpinLock { private SpinLock m_lock = new SpinLock(); public void Enter( ref bool isTaken ) { m_lock.Enter( ref isTaken ); } public void Exit() { m_lock.Exit(); } }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }
0 comments