Caching Enum.ToString to improve performance

 
 
  • Gérald Barré

Converting an enum value to a string using ToString() is expensive. In most cases, the performance impact is negligible. However, when ToString is called thousands of times per second, saving a few milliseconds matters.

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

Each of the following implementations lets you write the following code:

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

#Method 1: Using a dictionary

The first approach uses a Dictionary to cache the string value. This works for any enum type:

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

Performance improves significantly when using a dictionary typed to the specific enum (see the benchmark below):

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

If the enum values are sequential, you can replace the dictionary with an array for even better performance.

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 final approach hard-codes the mapping using a switch expression:

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 affected by the number of values in the enum. The benchmark tests enums with 4, 10, 50, and 100 values, and the results are consistent.

#Roslyn Source Generator

Writing these caching methods by hand is tedious. A better approach 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?