Caching Enum.ToString to improve performance
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.
enum Color
{
AliceBlue,
AntiqueWhite,
Aqua,
Aquamarine,
Azure,
...
}
Color value = Color.Aqua;
_ = value.ToString(); // 👈 we'll improve this one!
All of the following implementation allows to write the following code:
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:
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):
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.
// ⚠ 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
:
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
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
ToString | 31.8725 ns | 0.3661 ns | 0.3425 ns | 0.0076 | - | - | 24 B |
Dictionary | 26.8290 ns | 0.2416 ns | 0.2018 ns | 0.0076 | - | - | 24 B |
TypedDictionary | 6.4664 ns | 0.0582 ns | 0.0486 ns | - | - | - | - |
ConcurrentDictionary | 24.5398 ns | 0.2065 ns | 0.1831 ns | 0.0076 | - | - | 24 B |
TypedConcurrentDictionary | 9.8136 ns | 0.1064 ns | 0.0995 ns | - | - | - | - |
Array | 0.0000 ns | 0.0000 ns | 0.0000 ns | - | - | - | - |
Switch | 1.6659 ns | 0.0845 ns | 0.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):
[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!