BLOG
04 October 2022

Enums Go Fast: A Test Case

tech

It is undeniable that most of us use enums. And most people have a love/hate relationship with them. You can say enums are as fast as they possibly can be, but isn’t it just a biased statement? Or can they get any faster? We will check if it is true in this blog article.  

The “test” case 

Now that we have our statement to check, we want to test the enums, and thus we need a good test case. We’re going to test the speed of typical operations on a large enum and on a regular one. The large enum is going to be the one holding a list of all the countries. It can be found here. 

As for the small enum, it is going to be a short subset of all the countries. In my case it looks like this: 

1 2 3 4 5 6 7 public enum FewCountries { [Description("Poland")] PL = 177, [Description("Portugal")] PT = 178, [Description("Puerto Rico")] PR = 179, [Description("Qatar")] QA = 180, }

We are still missing a critical component, though, and to determine whether a strategy is fast, we must have something to compare it to. This is exactly what we are going to do now - the alternative for using enums in strander way will be to use a Git repository created just for that purpose. 

The Enum Generators 

The git repository I’m referring to is the one by Andrew Lock and can be found here. Please install it as a NugetPackage in your project.  

In my case the project is a console application, and my csproj looks like this: 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.2" /> <PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta04" /> </ItemGroup> </Project>

As you can see, I have two packages installed. One of them is the aforementioned Enum Generators and the second is BenchmarkDotNet - an excellent solution for benchmarking your application - the one I use myself and I strongly advise you to do the same. 

Let’s get back to Enums Generators. As you can see on the readme it is a Source Generator package that generates extension methods for enums to allow fast "reflection". That is exactly what it does, and I can guarantee you that it does it well. 

The base code 

First, we need to take a look at the enums we will be using. There are two of them. The big one... 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using System.ComponentModel; using NetEscapades.EnumGenerators; namespace ConsoleAppEnumTest; [EnumExtensions] public enum Countries { [Description("Afghanistan")] AF = 1, [Description("Åland Islands")] AX = 2, [Description("Albania")] AL = 3, [Description("Algeria")] DZ = 4, [Description("American Samoa")] AS = 5, [Description("Andorra")] AD = 6, [Description("Angola")] AO = 7, [Description("Anguilla")] AI = 8,

… and the small one. 

1 2 3 4 5 6 7 8 9 10 11 12 13 using System.ComponentModel; using NetEscapades.EnumGenerators; namespace ConsoleAppEnumTest; [EnumExtensions] public enum FewCountries { [Description("Poland")] PL = 177, [Description("Portugal")] PT = 178, [Description("Puerto Rico")] PR = 179, [Description("Qatar")] QA = 180, }

They are both standard enums: one with around 250 elements and one with 4. The one interesting thing about them is the EnumExtensions argument - this  argument is what makes them unique. It is provided by the previously mentioned Git repository. Before we look at how it works under the hood, let's have a look at the performance boost we're after. 

The speed 

We're using the benchmark approach to gauge our progress. I'll begin by benchmarking the small enum. To accomplish this, I created the class FewCountriesBenchmark

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using BenchmarkDotNet.Attributes; namespace ConsoleAppEnumTest; [MemoryDiagnoser] public class FewCountriesBenchmark { [Benchmark()] public string SlowToString() => FewCountries.PL.ToString(); [Benchmark()] public string FastToString() => FewCountries.PL.ToStringFast(); [Benchmark()] public bool SlowIsDefined() => Enum.IsDefined(typeof(FewCountries), (FewCountries)177); [Benchmark()] public bool FastIsDefined() => FewCountriesExtensions.IsDefined((FewCountries)177); [Benchmark()] public (bool, FewCountries) SlowTryParse() => (Enum.TryParse("PL", true, out FewCountries country), country); [Benchmark()] public (bool, FewCountries) FastTryParse() => (FewCountriesExtensions.TryParse("PL", true, out FewCountries country), country); }

 

As you can see, we're comparing three commonly used methods: ToString, IsDefined, and TryParse. Each of these methods is called by the built-in enum as well as the extension package. 

A similar class was created for the large enum. It was named CountriesBenchmark. A shocker, I know.  

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using BenchmarkDotNet.Attributes; namespace ConsoleAppEnumTest; [MemoryDiagnoser] public class CountriesBenchmark { [Benchmark()] public string SlowToString() => Countries.PL.ToString(); [Benchmark()] public string FastToString() => Countries.PL.ToStringFast(); [Benchmark()] public bool SlowIsDefined() => Enum.IsDefined(typeof(Countries), (Countries)177); [Benchmark()] public bool FastIsDefined() => CountriesExtensions.IsDefined((Countries)177); [Benchmark()] public (bool, Countries) SlowTryParse() => (Enum.TryParse("PL", true, out Countries country), country); [Benchmark()] public (bool, Countries) FastTryParse() => (CountriesExtensions.TryParse("PL", true, out Countries country), country); }

We need to change the configuration to release and run the application with Programs.cs looking like this in order to run the benchmarking process: 

  

1 2 3 4 5 6 7 8 // See https://aka.ms/new-console-template for more information using BenchmarkDotNet.Running; using ConsoleAppEnumTest; Console.WriteLine("Hello, World!"); BenchmarkRunner.Run<CountriesBenchmark>(); BenchmarkRunner.Run<FewCountriesBenchmark>();

After a few minutes we will be able to see the results and I must say they are very interesting. First lets look at the results for the smaller enum. As it was to be expected the extended version is much faster plus is doesn’t allocate any extra memory. It is so fast that the benchmarking IsDefined even gave us a warning: “FewCountriesBenchmark.FastIsDefined: Default -> The method duration is indistinguishable from the empty method duration”. Comparing to the out-of-the-box IsDefined, that also allocates memory it is a huge performance boost. The ToString and TryParse are also much faster.  

Now we'll go to the very big enum. The outcomes in this example are quite fascinating - in the instance of the first two methods, ToString and IsDefined, the performance of extension procedures is considerably better than that of out-of-the-box methods, both in terms of speed and memory. TryParse, on the other hand, is a different story, as the extended version is a little slower. Memory-wise, it's the same story. 

Under the hood  

We know that in most cases, the solution presented is much faster. I hope most of you can see the benefit of using this programme. But how exactly does it work? That is an excellent question. The magic begins when we add an Attribute. 

The magic is contained in the 2507 usages - the Attribute generated all of them when we added it. Expand the usages and pick one - no matter where you click, you will most likely end up with an auto-generated file. 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by the NetEscapades.EnumGenerators source generator // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ #nullable enable namespace ConsoleAppEnumTest { public static partial class CountriesExtensions { public static string ToStringFast(this ConsoleAppEnumTest.Countries value) => value switch { ConsoleAppEnumTest.Countries.AF => nameof(ConsoleAppEnumTest.Countries.AF), ConsoleAppEnumTest.Countries.AX => nameof(ConsoleAppEnumTest.Countries.AX), ConsoleAppEnumTest.Countries.AL => nameof(ConsoleAppEnumTest.Countries.AL), ConsoleAppEnumTest.Countries.DZ => nameof(ConsoleAppEnumTest.Countries.DZ), ConsoleAppEnumTest.Countries.AS => nameof(ConsoleAppEnumTest.Countries.AS),

This file contains all of the new methods, and it is the rewriting of the methods that provides the extra performance, so study it carefully and see what changes have been made. You can even consider it your homework. 

 

Summary     

We haincreased the speed of enums. Well, it was not exactly us who did it, but we did once utilise NuGet magic. Now, let's get serious. We used a scientific approach to evaluate our code once more. It was all about enums this time, and we observed how broadening the implementation can result in big gains. I hope that helps you and that we can all utilise it in our daily job. 

If you have any questions, please drop me a line at karol.rogowski@softwarehut.com. 

Till next time. Keep coding. 

 

 



Author
Karol Rogowski
Head Of Engineering

Working in IT since 2009. Currently working as Head of Engineering at SoftwareHut and as an academic teacher at Białystok Technical University. Co-founder of meet.js Białystok. Book and articles author. Father, husband, huge H.P. Lovecraft fan and terrible poker player.