Struct vs. Class, Safety vs. Speed

While at CodeMash, I had an interesting conversation with Cori Drew regarding some code in Effective C#, and some comments from Jon Skeet in our combined async talks. These comments involve breaking some common recommendations, and performance.

In our talk, Jon described how the C# compiler creates a mutable struct when it builds the state machine that handles async continuations. Jon discussed that the nested struct was faster than a nested class. Contrast that with code in Effective C#, where I showed the following code:

        public
        class List<T> : IEnumerable<T>
{
privateclass Enumerator<T> : IEnumerator<T>
{
// elided
}

public IEnumerator<T> GetEnumerator()
{
returnnew Enumerator<T>();
}

IEnumerator IEnumerable.GetEnumerator()
{
returnnew Enumerator<T>();
}
}

Well, Cori asked, why didn’t I make the Enumerator<T> a struct (which is what the BCL does):

        public
        class List<T> : IEnumerable<T>
{
privatestruct Enumerator<T> : IEnumerator<T>
{
// elided
}

public IEnumerator<T> GetEnumerator()
{
returnnew Enumerator<T>();
}

IEnumerator IEnumerable.GetEnumerator()
{
returnnew Enumerator<T>();
}
}

So, why didn’t I?

Well, as Eric Lippert points out, “mutable value types are evil.”  In general, you should avoid mutable structs. If your type requires mutation to work properly, you should use a reference type. Implementing IEnumerator requires mutation (keeping track of the current item), so you should use a reference (class) type. Unless you are really sure that you need a struct, you should use a class for any type that supports mutating operations.

Because of that, I chose to demonstrate with a class rather than a struct. I felt it would be too likely for readers to copy the code and use it in different situations where the special circumstances that are in play in both async state machines and the nested List iteration are no longer true. I chose to give up some performance in return for a more general idiom that would be correct in more situations.

Given that, let’s discuss why the mutable struct works in these two situations.

In the case of the nested Enumerator class, it’s because of one of the rules for converting a struct to an interface type. When a struct is converted to an interface, it isn’t actually unboxed. The box implements the interface, and adapts the methods defined on the interface by forwarding them to the struct in the box. The struct does not get unboxed and copied on each function call. See Section 11.3.5 of the C# spec for details. Read the following guidance carefully:

If your struct will always be accessed through an interface pointer, that struct can be safely mutable because it will never be unboxed.

The nested private Enumerator satisfies this requirement. It is a nested private type, so that other code cannot access it through the struct value. Client code can only access it through the interface reference, and therefore, the above rule always applies.

The nested structure that implements the state machine in an async scenario also follows a similar pattern, and therefore is safe.

OK, before you go just applying this rule to structs in your programs, read that guideline again carefully. Notice the strong ‘always’ and ‘never’. There can be no exceptions. In the case of the nested enumerator, this is enforced by the fact that GetEnumerator() returns the interface, not the struct. In the case of the nested struct in the async state machine, it’s enforced because it’s in compiler generated code.

You’re probably not so lucky. Chances are you work with other developers. Someday, someone will make some changes to your code so that one of those rules will be broken. Then, bugs will start to crop up.

Even if you are completely sure that those rules will never be violated, take care. Just because you can get away with something safely doesn’t mean you should turn away from normal guidance. In both these cases, the performance gains by changing from a class to a struct, can be significant. That significant performance gain coupled with the guaranteed safety does mean that its worth breaking the normal guidance.

In the case of the nested Enumerable, the performance gains come from the fact that this code will execute very often over the course of many programs.

In the case of the asyn methods, it’s because the team wants to optimize the ‘hot’ path for async methods. The reason is that you (and me) should prefer making a method async if there is a possibility that it will take enough time to warrant it. We shouldn’t worry that declaring a method async will cause an undo performance burden over its synchronous counterpart. With that in mind, the team is working very hard to optimize those code paths.

If you want to switch from a class to a struct, you should perform some measurements and make sure that the change will actually give you the increased performance you seek.

In the end, the same old advice often holds true: Make it work well. Then, once you’ve measured and determined that performance for a given location is critically important, make the changes necessary to achieve those goals. In my books, I wanted to try hard to write general guidance, and I avoided most performance based guidance. It seemed safer than trying to ensure that readers would remember all the specific rules for a particular optimization.

Created: 1/16/2012 5:03:46 PM

Current Projects

I create content for .NET Core. My work appears in the .NET Core documentation site. I'm primarily responsible for the section that will help you learn C#.

All of these projects are Open Source (using the Creative Commons license for content, and the MIT license for code). If you would like to contribute, visit our GitHub Repository. Or, if you have questions, comments, or ideas for improvement, please create an issue for us.

I'm also the president of Humanitarian Toolbox. We build Open Source software that supports Humanitarian Disaster Relief efforts. We'd appreciate any help you can give to our projects. Look at our GitHub home page to see a list of our current projects. See what interests you, and dive in.

Or, if you have a group of volunteers, talk to us about hosting a codeathon event.