Enums are great, but enums with performance are better

Enums are great, I use them all the time and I think all .NET developers use them, but they have a tradeoff: enums are not that performant because all the utility methods to work with them are relatively expensive in terms of performance.

Let's do a little benchmark to measure how these utility methods aren't optimized (we will compare these results later with other options; this benchmark was done on a console application with the .NET Framework 4.8):


|        Method |      Mean |    Error |   StdDev | Allocated |
|-------------- |----------:|---------:|---------:|----------:|
|  EnumToString | 324.46 ns | 4.764 ns | 4.456 ns |      36 B |
| EnumIsDefined |  66.95 ns | 0.877 ns | 0.820 ns |         - |
|  EnumTryParse | 211.29 ns | 1.256 ns | 1.175 ns |      68 B |
Enum ToString benchmark results

Now that we have put our finger on this issue, what can we do to fix it? The obvious solution would be to implement a caching system. We could store the label of each value of our enum in a collection (dictionary? array?) and get it from that collection rather than getting it from the enum itself. Our caching class could look something like this:


public static class ColorCachingExtensions
{
    private static IDictionary<Color, string> colorNames = new Dictionary<Color, string>();

    public static string ToStringWithCache(this Color color)
    {
        if (colorNames.ContainsKey(color))
        {
            return colorNames[color];
        }

        string colorName = color.ToString();
        colorNames.Add(color, colorName);
        return colorName;
    }
}

Let's see if we improved the performance and the memory allocation or not:

Benchmark with .NET 4.8:


|                Method |       Mean |     Error |     StdDev | Allocated |
|---------------------- |-----------:|----------:|-----------:|----------:|
|          EnumToString | 346.352 ns | 5.1282 ns |  4.2823 ns |      36 B |
| EnumToStringWithCache |  23.668 ns | 0.5214 ns |  0.7137 ns |         - |

Benchmark with .NET 6:


|                Method |      Mean |     Error |    StdDev | Allocated |
|---------------------- |----------:|----------:|----------:|----------:|
|          EnumToString | 25.039 ns | 0.5674 ns | 0.6306 ns |      24 B |
| EnumToStringWithCache | 16.416 ns | 0.2045 ns | 0.1913 ns |         - |

As you can see it works pretty well (15x faster with no memory allocation) but there are two tradeoffs doing something like that:

Another solution would be to create custom extension methods for our enumeration to implement the equivalent of the .ToString() method ourselves; it could be something like this:


public static class ColorExtensions
{
    public static string ToStringFast(this Color color)
        => color switch
        {
            Color.Green => nameof(Color.Green),
            Color.Blue => nameof(Color.Blue),
            Color.Red => nameof(Color.Red),
            Color.Yellow => nameof(Color.Yellow),
            Color.Cyan => nameof(Color.Cyan),
            Color.Pink => nameof(Color.Pink),
            _ => color.ToString(),
        };
}

As you can see, I just implemented a switch that returns the name of the value passed as an argument. Let's benchmark it to see how the performance compares to the previous ones:

Benchmark with .NET 4.8:


|                Method |       Mean |     Error |     StdDev | Allocated |
|---------------------- |-----------:|----------:|-----------:|----------:|
|          EnumToString | 346.352 ns | 5.1282 ns |  4.2823 ns |      36 B |
| EnumToStringWithCache |  23.668 ns | 0.5214 ns |  0.7137 ns |         - |
|      EnumToStringFast |   3.045 ns | 0.0508 ns |  0.0451 ns |         - |

Benchmark with .NET 6:


|                Method |      Mean |     Error |    StdDev | Allocated |
|---------------------- |----------:|----------:|----------:|----------:|
|          EnumToString | 25.039 ns | 0.5674 ns | 0.6306 ns |      24 B |
| EnumToStringWithCache | 16.416 ns | 0.2045 ns | 0.1913 ns |         - |
|      EnumToStringFast |  1.679 ns | 0.0782 ns | 0.0693 ns |         - |

As you can see, our extension method is 115x faster than the classic .ToString() method and has no memory allocation, way more performant! And we can implement the other methods as well.
The .IsDefined method could be something like this:


public static bool IsDefinedFast(this string color)
    => color switch
    {
        "Green" => true,
        "Blue" => true,
        "Red" => true,
        "Yellow" => true,
        "Cyan" => true,
        "Pink" => true,
        _ => false
    };

And the TryParse method could look like this:


public static bool TryParseFast(string color, out Color? parsedColor)
{
    parsedColor = null;
    switch (color)
    {
        case "Green":
            parsedColor = Color.Green;
            return true;
        case "Blue":
            parsedColor = Color.Blue;
            return true;
        case "Cyan":
            parsedColor = Color.Cyan;
            return true;
        case "Pink":
            parsedColor = Color.Pink;
            return true;
        case "Red":
            parsedColor = Color.Red;
            return true;
        case "Yellow":
            parsedColor = Color.Yellow;
            return true;
    }
    return false;
}

Benchmark with .NET 4.8:


|                Method |       Mean |     Error |     StdDev | Allocated |
|---------------------- |-----------:|----------:|-----------:|----------:|
|          EnumToString | 346.352 ns | 5.1282 ns |  4.2823 ns |      36 B |
| EnumToStringWithCache |  23.668 ns | 0.5214 ns |  0.7137 ns |         - |
|      EnumToStringFast |   3.045 ns | 0.0508 ns |  0.0451 ns |         - |
|         EnumIsDefined |  69.344 ns | 1.1880 ns |  1.4142 ns |         - |
|     EnumIsDefinedFast |  15.167 ns | 0.2528 ns |  0.3543 ns |         - |
|          EnumTryParse | 242.248 ns | 4.8671 ns | 12.8219 ns |      68 B |
|      EnumTryParseFast |  30.590 ns | 0.5771 ns |  0.5926 ns |         - |
Enum utility methods performance benchmark results (ToString(), IsDefined and TryParse)

Benchmark with .NET 6:


|                Method |      Mean |     Error |    StdDev | Allocated |
|---------------------- |----------:|----------:|----------:|----------:|
|          EnumToString | 25.039 ns | 0.5674 ns | 0.6306 ns |      24 B |
| EnumToStringWithCache | 16.416 ns | 0.2045 ns | 0.1913 ns |         - |
|      EnumToStringFast |  1.679 ns | 0.0782 ns | 0.0693 ns |         - |
|         EnumIsDefined | 84.807 ns | 1.4815 ns | 1.3858 ns |         - |
|     EnumIsDefinedFast |  2.753 ns | 0.0872 ns | 0.0773 ns |         - |
|          EnumTryParse | 76.595 ns | 1.3933 ns | 1.2351 ns |         - |
|      EnumTryParseFast | 11.209 ns | 0.1397 ns | 0.1307 ns |         - |
Enum utility methods performance benchmark results (ToString(), IsDefined and TryParse)

As you can see, the performance gains are massive. We already saw that the faster equivalent of the .ToString() method is more than 100 times faster than the classic method, but you can see that the IsDefined method is 5x faster than its equivalent and the TryParse method is 8 times faster than its equivalent, with no memory allocation against 68 bytes. It may seem small because we are talking about nanoseconds and bytes here, but multiply that by the number of times you work with enums and it can make a difference, especially if you have to work in limited environments like Arduinos or Raspberry Pis.

Our custom methods are way more efficient than the .NET ones but obviously our solution isn't perfect: it requires writing a specific extension class with several method implementations for each enumeration you are using. It's a lot of code, it must be maintained to avoid creating bugs and it can get quite laborious if you have to deal with big enumerations (my colors enumeration in the example has 6 values but the .NET one has 147 values, for example; imagine our class with that enum).

This is where Andrew Lock and Source Generators come in!

A Source Generator is a new kind of component that you can write to do several things, like generating C# source files that can be added to a compilation object during compilation. It means that you can provide some additional source files to the compilation while the source code is being compiled. It's exactly what we need: Source Generators can be used to create extension classes dynamically at compilation time, to write our methods for us. It's brilliant, and it's exactly what Andrew Lock did! Let me introduce you to the NetEscapades.EnumGenerators NuGet package.

NetEscapades.EnumGenerators

This little but absolutely brilliant NuGet package is a Source Generator that creates one extension class per enum that adds 7 useful methods to it that are much faster than their built-in equivalents:

To use it, you have to do two things:

  1. Add the package to your project
  2. Identify, with an attribute, the enums for which you want an extension class

Note that this NuGet package uses the .NET 6 incremental generator APIs, so you must have the .NET 6 SDK installed, though you can target earlier frameworks like .NET Core 3.1 or .NET Framework 4.8.

You can run the following command to install this package:


dotnet add package NetEscapades.EnumGenerators --prerelease

This adds a Package Reference to your project. You can additionally mark the package as PrivateAssets="all" and ExcludeAssets="runtime". Setting PrivateAssets="all" means any projects referencing this one won't get a reference to the NetEscapades.EnumGenerators package and setting ExcludeAssets="runtime" ensures the NetEscapades.EnumGenerators.Attributes.dll file is not copied to your build output because it is not required at runtime, the Source Generator operates during the compilation.

In a .NET Framework 4.8 project you will have a line like this in your packages.config file:


<package id="NetEscapades.EnumGenerators" version="1.0.0-beta04" targetFramework="net48" PrivateAssets="all" ExcludeAssets="runtime" />

To use the Source Generator on an enum, you have to add the EnumExtensions attribute to it:


[EnumExtensions]
public enum Color
{
    Green,
    Red,
    Blue,
    Yellow,
    Cyan,
    Pink
}

Now if you compile your project, NetEscapades.EnumGenerators will generate an extension class for each enum you marked with the EnumExtensions attribute, here is my ColorExtensions class:

NetEscapades.EnumGenerators extension class example

Let's do a little benchmark to see what the performance is now with the NetEscapades.EnumGenerators methods (benchmark on .NET 6):


|            Method |     Mean |     Error |    StdDev | Allocated |
|------------------ |---------:|----------:|----------:|----------:|
|  EnumToStringFast | 2.055 ns | 0.1037 ns | 0.0919 ns |         - |
| EnumIsDefinedFast | 3.765 ns | 0.0880 ns | 0.0735 ns |         - |
|  EnumTryParseFast | 9.916 ns | 0.1995 ns | 0.2375 ns |         - |
NetEscapades.EnumGenerators benchmark results on .NET 6

As you can see, NetEscapades.EnumGenerators is a really great and elegant solution to make enumerations performant which is a major thing because of the utility of enums.

Also, this package has a lot of features. For example, it takes enum attributes into account when generating the extension class, so you can still use the display name of enum values for example (not in 1.0.0-beta04, which was released in November 2021, but the development of this feature is ready to ship):


[EnumExtensions]
public enum Color
{
    Green,
    Red,
    Blue,
	
	[Display(Name = "It's yellow")]
    Yellow,
	
	[Display(Name = "Clear blue")]
    Cyan,
    Pink
}

You can also configure the extension class for an enum with a custom name and a custom namespace:


[EnumExtensions(ExtensionClassName ="MyCustomColorExtensionClass", ExtensionClassNamespace = "MyCustomColorExtensionClassNamespace")]

And now my class has another name and namespace:

You can check the GitHub of the project to see all the available options and features, and check the links below to dig deeper into NetEscapades.EnumGenerators (I recommend the post by Andrew Lock himself on his blog, which is really well written and detailed, and the video by Nick Chapsas).

Keep coding folks! :)

June 21, 2022
  • .NET
  • NetEscapades.EnumGenerators
  • Source Generator
  • .NET Enum
  • .NET 6