runtime: Enum.ToString is slower than explicit implementation
Description
Since Enum.ToString usually accesses Enum.GetEnumName, which accesses it in the runtime.
However, since the name is already given at compile time I think this should be treated as a constant and assembled at compile time.
Configuration
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1165 (21H1/May2021Update)
AMD Ryzen 5 3600, 1 CPU, 12 logical and 6 physical cores
.NET SDK=5.0.102
[Host] : .NET 5.0.2 (5.0.220.61120), X64 RyuJIT
DefaultJob : .NET 5.0.2 (5.0.220.61120), X64 RyuJIT
Data
Benchmarks
Method | Mean | Error | StdDev | Code Size | Gen 0 | Allocated |
---|---|---|---|---|---|---|
RegularToString | 35.004 ns | 0.5859 ns | 0.5481 ns | 104 B | 0.0029 | 24 B |
EnumGetName | 72.600 ns | 0.8403 ns | 0.7860 ns | 68 B | 0.0029 | 24 B |
ExplicitToStringImplementation | 1.764 ns | 0.0613 ns | 0.0573 ns | 209 B | - | - |
References
Enum.GetEnumName IL
.method assembly hidebysig static string
GetEnumName(
class System.RuntimeType enumType,
unsigned int64 ulValue
) cil managed
{
.maxstack 8
// [108 13 - 108 64]
IL_0000: ldarg.0 // enumType
IL_0001: ldc.i4.1
IL_0002: call class System.Enum/EnumInfo System.Enum::GetEnumInfo(class System.RuntimeType, bool)
IL_0007: ldarg.1 // ulValue
IL_0008: call string System.Enum::GetEnumName(class System.Enum/EnumInfo, unsigned int64)
IL_000d: ret
}
Extension method IL
.method public hidebysig static string
ExplicitToString(
valuetype ToStringBenchmarks.ExampleEnum 'enum'
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
= (01 00 00 00 )
.maxstack 3
.locals init (
[0] string V_0
)
// [9 13 - 16 15]
IL_0000: ldarg.0 // 'enum'
IL_0001: switch (IL_001c, IL_0024, IL_002c, IL_0034, IL_003c)
IL_001a: br.s IL_0044
// [10 41 - 10 69]
IL_001c: ldstr "VariantA"
IL_0021: stloc.0 // V_0
IL_0022: br.s IL_0056
// [11 41 - 11 69]
IL_0024: ldstr "VariantB"
IL_0029: stloc.0 // V_0
IL_002a: br.s IL_0056
// [12 41 - 12 69]
IL_002c: ldstr "VariantC"
IL_0031: stloc.0 // V_0
IL_0032: br.s IL_0056
// [13 41 - 13 69]
IL_0034: ldstr "VariantD"
IL_0039: stloc.0 // V_0
IL_003a: br.s IL_0056
// [14 41 - 14 69]
IL_003c: ldstr "VariantE"
IL_0041: stloc.0 // V_0
IL_0042: br.s IL_0056
// [15 22 - 15 87]
IL_0044: ldstr "enum"
IL_0049: ldarg.0 // 'enum'
IL_004a: box ToStringBenchmarks.ExampleEnum
IL_004f: ldnull
IL_0050: newobj instance void [System.Runtime]System.ArgumentOutOfRangeException::.ctor(string, object, string)
IL_0055: throw
IL_0056: ldloc.0 // V_0
IL_0057: ret
}
We see the IL is bigger, but the code is faster.
Analysis
It is because in runtime the value is resolved although it is already known at compile time.
Perhaps it would also make sense to add a valuenameof
keyword similar to nameof
with the difference that valuenameof
is taken the name of the corresponding enum variant.
External references
- YouTube Video by @Elfocrash: C#'s Enum performance trap your code is suffering from
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 4
- Comments: 28 (21 by maintainers)
Hey everyone, just jumping in to give my two cents since I made the video in the first place and I don’t wanna be taken out of context.
Bit of a background first. I didn’t discover this, I actually attended @davidfowl’s talk at IO Associates and he mentions this very use-case, the switch being a solution and addresses the O(n) and O(log(n)) thing. All I did is take that, wrap it in a good story and a clickable title and publish it.
Now as I mentioned in the video, people don’t have to do this until the need to, which basically eliminates the vest majority of C# devs. Do I have usecases for it personally? Sure but that’s just another thing in my micro optimization “checklist”. So is not using LINQ, eliminating heap allocations when possible, preventing boxing and a bunch of other stuff.
Realistically the only thing I wanted out of that video was for someone to take the time and create a source generator that can do this automatically at compile time so I don’t have to create switches by hand, and I got that. That’s all there is to it.
This is a bug in Roslyn compiler. I have opened an issue for it: https://github.com/dotnet/roslyn/issues/70841
To place things in context I created a small test. Of course, when comparing the switched optimization directly to
Enum.ToString
(‘from which your code is suffering’), the 6x difference seems significant. But please note that once you put it into some real-life scenario (eg. logging), the cost of the conversion becomes negligible:Please also note that:
KnownColor
instead of the usedConsoleColor
) because of the sequential execution of the casesDisclaimer: The linked test uses a very short warm-up period to spare the resources of the .NET Fiddle site. To get more reliable results (with low worst-best differences) execute it on your computer with default configuration. The test needs this package (and as I mentioned in my last comment it happens to have some solution for faster enum handling but on recent .NET platforms I would not say it means a huge benefit).
Can’t this be a Roslyn code analyzer and fixer under the “performance” category? If the analyzer sees
SomeEnum.SomeValue.ToString()
, it recommends a replacement ofnameof(SomeEnum.SomeValue)
? Ignore flags enums for now since they complicate matters a bit.Note: This could in theory be a behavioral breaking change if new values are introduced to the enum between compile time and runtime. If the enum is defined in the same assembly as the call site, this obviously isn’t an issue. But I don’t think this would happen in practice often enough to be worth worrying about.
This could simply be a source generator.
So I understand this issue doesn’t have much love, but just wanted to add something I was experimenting with.
@stephentoub mentioned:
That’s exactly what I did in this NuGet Package and it works great 🙂
However, I was interested playing with the interceptors support in .NET 8. So I put together a simple example program:
There’s a couple of interesting things here:
System.Enum
as the interceptor argument, notMyEnum
InvalidProgramException
I can understand that there are deep CLR reasons why this won’t work, but a runtime
InvalidProgramException
doesn’t feel like the right behaviour here 😅You can’t override ToString on an enum in C#, which is what a C# source generator would output. And we can’t / wouldn’t want to modify the base Enum.ToString to know about every enum in the universe, nor would we want it to use some kind of reflection to find and hook up to some well-known-named method that provided this functionality. Having a heavily-optimized ToString implementation that comes at the expense of a non-trivial amount of additional IL (especially for larger enums) is not something that 99.9% of enums require. As @KalleOlaviNiemitalo outlined, a source generator could output an extension method for a specific enum, based for example on attributing that enum with some attribute the source generator knows about, and any code paths that need the super-fast access could call that extension method. I do not currently see a strong need for such a source generator to be in the .NET SDK; if a source generator here is something you’re interested in pursuing, I suggest you do it in some other repo, release it as a nuget for others to consume, etc. Only a small percentage of valuable source generators need actually be in the .NET SDK.
AFAIK, Roslyn source generators currently can only add source code, not remove or modify any. So if the original source code contains an enum type definition, then it will be compiled. C# enum type definitions cannot be partial, and according to ECMA-335 6th edition sections I.8.5.2 (Assemblies and scoping) and II.22.37 (TypeDef : 0x02), enum types cannot define methods of their own. So the source generator cannot inject an override ToString() method in the enum type. It could generate a type with an extension method, but if the extension method were named ToString(), then C# would not use it because the enum type inherits Enum.ToString() already. So the best that the source generator could do in the current SDK would be to generate a class with a differently-named extension method, e.g. ToStringFast(), and recommend its use with an analyzer and a code fixer. It might be possible to automatically use this for interpolated strings, though.
Yeah, in 6.0 source generators were added for JSON serializers and for logging.