This article benchmarks various ways to obtain a string from an enumeration value in C# .NET, along with other common enumeration APIs.
At NDepend, we value your time, so here are our key findings:
- The library NetEscapades.EnumGenerators generates code at compile time for faster enumeration APIs, including enum to string conversions. This generated code outperforms standard .NET enumeration APIs and other libraries benchmarked here. To use it, just set the attribute
[EnumExtensions]
on each enum declaration, and a class namedEnumNameExtensions
in the anonymous namespace is generated for each enum. - If the enumeration value is known at compile time,
nameof(MyEnum.MyValue)
prompts the compiler to emit the string directly, making it the fastest option available in this situation. - .NET 8 brings substantial performance optimizations for enumerations. If you haven’t updated yet, we highly recommend doing so. However, the code generated by NetEscapades.EnumGenerators still outperforms .NET 8 optimizations.
The Benchmark
Let’s benchmark these enum APIs:
- System.Enum class (.NET 8)
- FastEnum package
- Enums.NET package
- NetEscapades.EnumGenerators package
- Meziantou.Framework.FastEnumToStringGenerator package
To do so we rely on BenchmarkDotNet. The .csproj
project file 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>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.12" /> <PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta09" /> <PackageReference Include="FastEnum" Version="1.8.0" /> <PackageReference Include="Enums.NET" Version="5.0.0" /> <PackageReference Include="Meziantou.Framework.FastEnumToStringGenerator" Version="2.0.1" /> </ItemGroup> </Project> |
Here is the benchmark C# source code:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; using FastEnumUtility; using NetEscapades.EnumGenerators; using EnumsNET; [assembly: FastEnumToString(typeof(Day))] // For Meziantou.Framework.FastEnumToStringGenerator BenchmarkRunner.Run<Benchmarks>(); [EnumExtensions] // To generate class DayExtensions with NetEscapades.EnumGenerators public enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday } [MemoryDiagnoser] [HideColumns("StdDev", "Median", "Job", "RatioSD", "Error", "Gen0", "Alloc Ratio")] public class Benchmarks { // Use random day to avoid compiler optimization private static readonly Day _day = (Day)new Random((int)DateTime.UtcNow.Ticks).Next(Enum.GetValues(typeof(Day)).Length); private static readonly string _dayStr = _day.ToString(); [Benchmark] public string ToString() => _day.ToString(); [Benchmark] public string _nameof_Constant() => nameof(Day.Saturday); [Benchmark] public string? GetName() => Enum.GetName(_day); [Benchmark] public string GetName_NetEscapades() => DayExtensions.ToStringFast(_day); // <- Extension method [Benchmark] public string? GetName_FastEnum() => FastEnum.GetName(_day); [Benchmark] public string? GetName_EnumsNET() => _day.GetName(); [Benchmark] public string? GetName_Meziantou() => FastEnumToStringExtensions.ToStringFast(_day); // <- Extension method [Benchmark] public bool TryParse() => Enum.TryParse(_dayStr, out Day day); [Benchmark] public bool TryParse_NetEscapades() => DayExtensions.TryParse(_dayStr, out Day day); [Benchmark] public bool TryParse_FastEnum() => FastEnum.TryParse(_dayStr, out Day day); [Benchmark] public bool TryParse_EnumsNET() => Enums.TryParse(_dayStr, out Day day); [Benchmark] public bool IsDefined() => Enum.IsDefined(_day); [Benchmark] public bool IsDefined_NetEscapades() => DayExtensions.IsDefined(_day); [Benchmark] public bool IsDefined_FastEnum() => FastEnum.IsDefined(_day); [Benchmark] public bool IsDefined_EnumsNET() => Enums.IsDefined(_day); [Benchmark] public string[] GetNames() => Enum.GetNames<Day>(); [Benchmark] public string[] GetNames_NetEscapades() => DayExtensions.GetNames(); [Benchmark] public IReadOnlyList<string> GetNames_FastEnum() => FastEnum.GetNames<Day>(); [Benchmark] public IReadOnlyList<string> GetNames_EnumsNET() => Enums.GetNames<Day>(); [Benchmark] public Array GetValues() => Enum.GetValues<Day>(); [Benchmark] public Array GetValues_NetEscapades() => DayExtensions.GetValues(); [Benchmark] public IReadOnlyList<Day> GetValues_FastEnum() => FastEnum.GetValues<Day>(); [Benchmark] public IReadOnlyList<Day> GetValues_EnumsNET() => Enums.GetValues<Day>(); } |
Here are the results:
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 26 27 28 29 |
| Method | Mean | Allocated | |----------------------- |-----------:|----------:| | ToString | 10.8759 ns | 24 B | | _nameof_Constant | 0.3759 ns | - | | GetName | 2.8809 ns | - | | GetName_NetEscapades | 0.9034 ns | - | | GetName_FastEnum | 1.1815 ns | - | | GetName_EnumsNET | 0.9911 ns | - | | GetName_Meziantou | 0.8997 ns | - | | | | | | TryParse | 23.5549 ns | - | | TryParse_NetEscapades | 3.2275 ns | - | | TryParse_FastEnum | 8.7912 ns | - | | TryParse_EnumsNET | 27.2358 ns | - | | | | | | IsDefined | 1.9848 ns | - | | IsDefined_NetEscapades | 0.0008 ns | - | | IsDefined_FastEnum | 0.3041 ns | - | | IsDefined_EnumsNET | 0.1107 ns | - | | | | | | GetNames | 14.9214 ns | 80 B | | GetNames_NetEscapades | 7.1788 ns | 80 B | | GetNames_FastEnum | 0.6397 ns | - | | GetNames_EnumsNET | 1.4361 ns | - | | | | | | GetValues | 34.7867 ns | 56 B | | GetValues_NetEscapades | 5.6920 ns | 56 B | | GetValues_FastEnum | 0.6432 ns | - | | GetValues_EnumsNET | 1.7573 ns | - | |
The outcomes are:
-
nameof(MyEnum.MyValue)
is the fastest method. This isn’t surprising since the compiler directly emits the string"Saturday"
without requiring a runtime lookup. However, if we usenameof(_day)
, the compiler emits the string"_day"
, which is the name of the variable, not the name of the day that the variable_day
references. - To obtain a string dynamically from an enum value, both Meziantou.Framework.FastEnumToStringGenerator and NetEscapades.EnumGenerators are slightly better than other libraries. Both packages emit this code at compile time:
1 2 3 4 5 6 7 8 9 10 11 12 |
public static string ToStringFast(this global::Day value) { return value switch { Day.Sunday => nameof(Day.Sunday), Day.Monday => nameof(Day.Monday), Day.Tuesday => nameof(Day.Tuesday), Day.Wednesday => nameof(Day.Wednesday), Day.Thursday => nameof(Day.Thursday), Day.Friday => nameof(Day.Friday), Day.Saturday => nameof(Day.Saturday), _ => value.ToString(), }; } |
- On the other hand, both Enums.NET and FastEnum don’t emit code. Their strategy is to cache the values in memory at runtime.
- Meziantou.Framework.FastEnumToStringGenerator doesn’t generate code for other enumeration APIs.
- NetEscapades.EnumGenerators is the fastest to parse a string to attempt to obtain an enumeration value.
- NetEscapades.EnumGenerators is the fastest for
IsDefined()
. It generates an unsafe and generic method that relies onSystem.RuntimeType
. - FastEnum is the fastest for both
GetNames()
andGetValues()
. It caches in memory the array while NetEscapades.EnumGenerators build a new array at each invocation.
.NET 8.0 improvements compared to .NET 7.0
Let’s benchmark .NET 8.0 improvements against .NET 7.0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
| Method | Runtime | Mean | Ratio | Allocated | |----------------- |--------- |------------:|------:|----------:|- | ToString | .NET 7.0 | 17.7319 ns | 1.00 | 24 B | | ToString | .NET 8.0 | 10.0987 ns | 0.57 | 24 B | | | | | | | | GetName | .NET 7.0 | 14.8490 ns | 1.00 | - | | GetName | .NET 8.0 | 2.8323 ns | 0.19 | - | | | | | | | | _nameof_Constant | .NET 7.0 | 0.7680 ns | 1.00 | - | | _nameof_Constant | .NET 8.0 | 0.5032 ns | 0.66 | - | | | | | | | | TryParse | .NET 7.0 | 37.1569 ns | 1.00 | - | | TryParse | .NET 8.0 | 24.1377 ns | 0.65 | - | | | | | | | | IsDefined | .NET 7.0 | 13.5955 ns | 1.00 | - | | IsDefined | .NET 8.0 | 1.9475 ns | 0.14 | - | | | | | | | | GetNames | .NET 7.0 | 22.9313 ns | 1.00 | 80 B | | GetNames | .NET 8.0 | 15.3411 ns | 0.67 | 80 B | | | | | | | | GetValues | .NET 7.0 | 501.1724 ns | 1.00 | 224 B | | GetValues | .NET 8.0 | 35.1934 ns | 0.07 | 56 B | |
This is the .csproj
file content:
1 2 3 4 5 6 7 8 9 10 11 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFrameworks>net7.0;net8.0</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.12" /> </ItemGroup> </Project> |
Here is the benchmark source code.
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 26 |
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; BenchmarkRunner.Run<Benchmarks>(); public enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday } [SimpleJob(RuntimeMoniker.Net70, baseline: true)] [SimpleJob(RuntimeMoniker.Net80)] [MemoryDiagnoser] [HideColumns("StdDev", "Median", "Job", "RatioSD", "Error", "Gen0", "Alloc Ratio")] public class Benchmarks { // Use random day to avoid compiler optimization private static readonly Day _day = (Day)new Random((int)DateTime.UtcNow.Ticks).Next(Enum.GetValues(typeof(Day)).Length); private static readonly string _dayStr = _day.ToString(); [Benchmark] public string ToString() => _day.ToString(); [Benchmark] public string? GetName() => Enum.GetName(_day); [Benchmark] public string _nameof_Constant() => nameof(Day.Saturday); [Benchmark] public bool TryParse() => Enum.TryParse<Day>(_dayStr, out Day day); [Benchmark] public bool IsDefined() => Enum.IsDefined(_day); [Benchmark] public string[] GetNames() => Enum.GetNames<Day>(); [Benchmark] public Array GetValues() => Enum.GetValues<Day>(); } |
Noteworthy NetEscapades.EnumGenerators Features
Given that the benchmark results recommend using NetEscapades.EnumGenerators, here are some of its interesting features:
- Only with NetEscapades.EnumGenerators you can use the attribute System.ComponentModel.DataAnnotations.DisplayAttribute or (exclusive) System.ComponentModel.DescriptionAttribute to define a string different than the enumeration value name. This is especially useful if your code gets obfuscated because the obfuscator will change enum names embedded in the DLL.
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 FastEnumUtility; using NetEscapades.EnumGenerators; using System.ComponentModel.DataAnnotations; using EnumsNET; [assembly: FastEnumToString(typeof(MyEnum))] Assert.IsTrue(nameof(MyEnum.Second) == "Second"); Assert.IsTrue(MyEnum.Second.ToString() == "Second"); Assert.IsTrue(Enum.GetName(MyEnum.Second) == "Second"); Assert.IsTrue(MyEnumExtensions.ToStringFast(MyEnum.Second) == "2nd"); Assert.IsTrue(FastEnum.GetName(MyEnum.Second) == "Second"); Assert.IsTrue(MyEnum.Second.GetName() == "Second"); // Enum.NET Assert.IsTrue(FastEnumToStringExtensions.ToStringFast(MyEnum.Second) == "Second"); [EnumExtensions] public enum MyEnum { First, [Display(Name = "2nd")] Second, } static class Assert { public static void IsTrue(bool b) { System.Diagnostics.Debug.Assert(b); } } |
- NetEscapades.EnumGenerators generates a
TryParse(...)
overload withbool ignoreCase
andallowMatchingMetadataAttribute
:
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 NetEscapades.EnumGenerators; using System.ComponentModel.DataAnnotations; Assert.IsTrue(MyEnumExtensions.TryParse("SECOND", out MyEnum val1, ignoreCase: true)); Assert.IsTrue(val1 == MyEnum.Second); Assert.IsFalse(MyEnumExtensions.TryParse("SECOND", out _, ignoreCase: false)); // Beware the Display value cannot be used for parsing... Assert.IsFalse(MyEnumExtensions.TryParse("2nd", out _)); // ..unless you set allowMatchingMetadataAttribute Assert.IsTrue(MyEnumExtensions.TryParse("2nd", out val1, ignoreCase: false, allowMatchingMetadataAttribute: true)); Assert.IsTrue(val1 == MyEnum.Second); [EnumExtensions] public enum MyEnum { First, [Display(Name = "2nd")] Second, } static class Assert { public static void IsTrue(bool b) { System.Diagnostics.Debug.Assert(b); } public static void IsFalse(bool b) { System.Diagnostics.Debug.Assert(!b); } } |
-
When you create an enum decorated with the
[Flags]
attribute, an additional method is generated that offers a bitwise alternative to theEnum.HasFlag(flag)
method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using NetEscapades.EnumGenerators; Assert.IsTrue(MyEnum.All.HasFlagFast(MyEnum.Second)); Assert.IsFalse(MyEnum.None.HasFlagFast(MyEnum.Second)); [EnumExtensions] [Flags] public enum MyEnum { None = 0x00, First = 0x01, Second = 0x02, All = 0x03 } static class Assert { public static void IsTrue(bool b) { System.Diagnostics.Debug.Assert(b); } public static void IsFalse(bool b) { System.Diagnostics.Debug.Assert(!b); } } |