Mutable value types: the good, the bad and the ugly

Mutable value types: the good, the bad and the ugly

Rate This
  • Comments 11

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();
      }
}
Leave a Comment
  • Please add 7 and 4 and type the answer here:
  • Post
  • PingBack from http://asp-net-hosting.simplynetdev.com/mutable-value-types-the-good-the-bad-and-the-ugly/

  • PingBack from http://microsoft-sharepoint.simplynetdev.com/mutable-value-types-the-good-the-bad-and-the-ugly/

  • hi,

    maybe the 'SafeSpinLock' is a good candidate for a class that will become a part of .net framework?

    just in case that we will have 100+ different discussions out there how to implement it: initialise it, dispose in controlled manner, pass around, etc..

  • Why not just use the already present Nullable<T> wrapper?

    SpinLock? spinWrapper;

    done :)

  • @Nick, Nullable<T> is still a value type (struct) with copy-by-value semantics, not a reference type, like the sample wrapper class.

  • Its interesting that you reference a post from Eric Lippert's blog. Eric has written quite a few blog posts about the importance of choosing value versus reference types based on their semantics, not their implementation. The fact that value types are allocated inline and reference types in their own space on the heap is an implementation detail that should be irrelevant to the design of the type. Performance characteristics change with time and environment, but type semantics are fixed. I am curious why you felt it was necessary to break this rule of design for types like SpinLock and SpinWait.

  • commongenius,

    Apologies for the slow response.  You're concern is quite valid and I assure you we spent a lot of time debating the tradeoffs before we came to a decision.  

    Ultimately, while we try our best to avoid implementation details there are just some scenarios that we wouldn't have been able to enable if our types required object allocations. When it comes down to it, we felt that our target audience would often throw aside the types we provided and build their own value-type versions that would give them the performance they need. Of course, we introduce new issues, such as the ones described in this post but we think the developers that would use these types are willing to overcome those issues if it meant they didn't need to build their own types from scratch.

    Josh

  • Josh-

    Since value types are typically alive as long as the stack frame on which they are allocated is active(i.e the stack frame the currently executing thread is running on) and are de-allocated(or fall out of scope) when the stack goes away.

    What is the lifetime of a value type(such as SpinLock )when it's wrapped by a reference type(such as SafeSpinLock), are lifetimes of value types the same as the lifetime  of the enclosing reference type in this case?

  • Josh-

    "when we retrieve a partition lock via List’s indexer, we actually get a new copy of a SpinLock "

    why would indexing into a List<T> give you a new value type. value types are not boxed when using List<T>

    Also, I ran the following and I noticed that I get the same hashcode for all SpinLocks, can you shed some light on why this is so?

    List<SpinLock> list = new List<SpinLock>();

               var sl1 = new SpinLock();

               var sl2 = new SpinLock();

               var sl3 = new SpinLock();

               Console.WriteLine(string.Format("Item1 Hashcode {0}",sl1.GetHashCode()));

               Console.WriteLine(string.Format("Item2 Hashcode {0}", sl2.GetHashCode()));

               Console.WriteLine(string.Format("Item3 Hashcode {0}", sl3.GetHashCode()));

               Console.WriteLine("\n");

               list.Add(sl1);

               list.Add(sl2);

               list.Add(sl3);

               ThreadPool.QueueUserWorkItem(_ =>

                   {

                       var str = string.Format("ThreadID:{0} reading item at index 0,hashcode{1}", Thread.CurrentThread.ManagedThreadId, list[0].GetHashCode());

                       Console.WriteLine(str);

                   }

               );

               ThreadPool.QueueUserWorkItem(_ =>

                   {

                        var str = string.Format("ThreadID:{0} reading item at index 1,hashcode{1}", Thread.CurrentThread.ManagedThreadId, list[1].GetHashCode());

                        Console.WriteLine(str);

                   }            

               );

               ThreadPool.QueueUserWorkItem(_ =>

                   {

                       var str = string.Format("ThreadID:{0} reading item at index 2,hashcode{1}", Thread.CurrentThread.ManagedThreadId, list[2].GetHashCode());

                       Console.WriteLine(str);

                   }

               );

               //list.ForEach( i=> Console.WriteLine(i.GetHashCode()));

               Console.Read();

    --------

    Output:

    Item1 Hashcode -1863199827

    Item2 Hashcode -1863199827

    Item3 Hashcode -1863199827

    ThreadID:3 reading item at index 0,

    hashcode -1863199827

    ThreadID:4 reading item at index 1,

    hashcode -1863199827

    ThreadID:3 reading item at index 2,

    hashcode -1863199827

  • Hi Abhijeet,

    Yes, when you box a value type, they become stored on the heap and, thus, have the same lifetime as the garbage-collected wrapper.  

    Regarding List's indexer: It is because the value types are not boxed that a new copy is given out every time.  If the value-types were boxed, they would be wrapped in a reference and that same reference would be passed out of the indexer on each access.  

    Regarding hash codes:  See the documentation for Object.GetHashCode. "The default implementation of the GetHashCode method does not guarantee unique return values for different objects."  In fact, for some structs in the BCL, even the overriden GetHashCode function does not return unique values for different objects (try it on Int32).  For SpinLock, we chose not to override the default GetHashCode and provide an accurate hashing function simply because we don't want to encourage people to use them like classes and pass them around.  Hash functions are typically used to store objects in structures like hash tables, and since that's a bad practice for the reasons listed in this post, we don't want to send mixed signals by actually implementing the method.

    Thanks for the questions! Hope that helps.

    Josh

  • Thanks for responding.

    I see what you mean,so unless there were a way to pass a reference to the SpinLock contained "inside" the List<SpinLock>, you would always get a copy when you index, and hence the bug.

    Just a thought...if the SpinLock struct implemented an ISpinLock and you had a List<ISpinLock>, you would be able to access the same SpinLock instance via the interface?

Page 1 of 1 (11 items)