Caching Enum.ToString to improve performance

 
 
  • Gérald Barré

Converting an enum value to a string using the ToString() method is expensive. In general, the performance impact is negligible. But when you call the ToString method thousands of times per second, saving a few milliseconds is important.

C#
enum Color
{
    AliceBlue,
    AntiqueWhite,
    Aqua,
    Aquamarine,
    Azure,
    ...
}
C#
Color value = Color.Aqua;
_ = value.ToString(); // 👈 we'll improve this one!

All of the following implementation allows to write the following code:

C#
Color value = Color.Aqua;
_ = value.ToStringCached(); // 🚀🚀🚀

#Method 1: Using a dictionary

The first way to improve performance is to use a Dictionary to store the value. This works for any enumeration value:

C#
public static class EnumExtensions
{
    // You can use a Dictionary for single-threaded application
    private static readonly ConcurrentDictionary<Enum, string> s_cache = new ConcurrentDictionary<Enum, string>();

    public static string ToStringCached(this Enum value)
    {
        return s_cache.GetOrAdd(value, v => v.ToString());
    }
}

#Method 2: Using a specialized dictionary

You can greatly improve the performance by using a dictionary specific to the enum type (see benchmark at the end):

C#
public static class EnumExtensions
{
    // You can use a Dictionary for single-threaded application
    private static readonly ConcurrentDictionary<Color, string> _cache = new ConcurrentDictionary<Color, string>();

    public static string ToStringCached(this Color value)
    {
        return _cache.GetOrAdd(value, v => v.ToString());
    }
}

#Method 3: Using an array

Another optimization is possible if the values are sequential. In this case, you can replace the dictionary with an array.

C#
// ⚠ Only works if the enum values are sequential
// Also, this implementation doesn't support flags enum or undefined values
public static class EnumExtensions
{
    private static readonly string[] s_enumStringValues = GetEnumStrings();

    private static string[] GetEnumStrings()
    {
        System.Collections.IList list = Enum.GetValues(typeof(Color));

        var result = new string[list.Count];
        for (int i = 0; i < list.Count; i++)
        {
            result[i] = list[i].ToString();
        }

        return result;
    }

    public static string ToStringCached(this Color myEnum)
    {
        return s_enumStringValues[(int)myEnum]; // If the first value is not 0, you need to adapt the logic: ((int)myEnum - MyEnum.FirstValue)
    }
}

#Method 4: Using a switch

The last solution is to hard-code the method using a switch:

C#
public static class EnumExtensions
{
    public static string ToStringCached(this Color myEnum)
    {
        return myEnum switch
        {
            Color.AliceBlue => nameof(Color.AliceBlue),
            Color.AntiqueWhite => nameof(Color.AntiqueWhite),
            Color.Aqua => nameof(Color.Value0),
            Color.Aquamarine => nameof(Color.Aquamarine),
            Color.Azure => nameof(Color.Azure),
            ...
            _ => myEnum.ToString(),
        }
    }
}

#Benchmark

The source of the benchmark is available here.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i5-6600 CPU 3.30GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=5.0.200-preview.20601.7
  [Host]    : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT
  RyuJitX64 : .NET Core 5.0.2 (CoreCLR 5.0.220.61120, CoreFX 5.0.220.61120), X64 RyuJIT

Job=RyuJitX64  Jit=RyuJit  Platform=X64
MethodMeanErrorStdDevGen 0Gen 1Gen 2Allocated
ToString31.8725 ns0.3661 ns0.3425 ns0.0076--24 B
Dictionary26.8290 ns0.2416 ns0.2018 ns0.0076--24 B
TypedDictionary6.4664 ns0.0582 ns0.0486 ns----
ConcurrentDictionary24.5398 ns0.2065 ns0.1831 ns0.0076--24 B
TypedConcurrentDictionary9.8136 ns0.1064 ns0.0995 ns----
Array0.0000 ns0.0000 ns0.0000 ns----
Switch1.6659 ns0.0845 ns0.0791 ns----

The results are not impacted by the number of values in the enumeration. The actual benchmark tests enumerations with 4, 10, 50, and 100 values and results are similar.

#Roslyn Source Generator

I'm pretty sure you don't want to write the following code by hand. A better solution is to use a Roslyn Source Generator to generate the method automatically. You can use the package Meziantou.Framework.FastEnumToStringGenerator (NuGet package):

C#
[assembly: FastEnumToString(typeof(Sample.Color))]

namespace Sample
{
    public enum Color
    {
        Blue,
        Red,
        Green,
    }

    class Program
    {
        static void Main()
        {
            Color color = Color.Green;
            System.Console.WriteLine(color.ToStringFast());
        }
    }
}

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub