-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Description
Although the Enum code performance has been greatly improved throughout .NET Core and a lot of the worst offenders (HasFlag) have been improved there are still clearly performance gains to be had. In our MSInternal service we still use a home brewed EnumHelper class for our frequent use of TryParse and ToString. Although performance on the default implementations has improved in recent .NET versions it's still significantly less performant as our home brewed solution and even slower compared to some widely available libraries I've also tested FastEnum and Enums.NET. The downside of including such libraries are the same as always, increasing complexity of dependency graphs and the question of ownership and maintainability.
Configuration
Production scenario:
net6.0
Windows Server Evergreen
x64
Various
Benchmarking scenario:
net6.0 and net7.0 as labeled
Windows 11 22H2
x64
i7 8700k
Regression?
No
Data
These charts contain four different paths, the default .NET functions, our homebrewed solution that involves a little caching on top of the default implementation, and then the paths of two libraries FastEnum and Enums.NET. The Parse methods include two tests for each, one where a string is provided and one where only a ReadOnlySpan is provided so implementations that can work well with spans are preferred.
ToString net6.0
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|
| Default | 5,772.3 us | 115.05 us | 127.88 us | 585.9375 | 203.1250 | 203.1250 | 3125.82 KB |
| EnumHelper | 679.0 us | 6.74 us | 6.30 us | 179.6875 | 179.6875 | 179.6875 | 781.42 KB |
| FastEnum | 633.7 us | 8.35 us | 7.41 us | 181.6406 | 181.6406 | 181.6406 | 781.39 KB |
| EnumsDotnet | 664.7 us | 9.78 us | 9.14 us | 181.6406 | 181.6406 | 181.6406 | 781.38 KB |
ToString net7.0
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|
| Default | 4,859.5 us | 86.12 us | 199.59 us | 562.5000 | 187.5000 | 187.5000 | 3125.69 KB |
| EnumHelper | 736.1 us | 7.41 us | 6.93 us | 179.6875 | 179.6875 | 179.6875 | 781.4 KB |
| FastEnum | 747.4 us | 14.82 us | 27.48 us | 181.6406 | 181.6406 | 181.6406 | 781.42 KB |
| EnumsDotnet | 779.1 us | 14.32 us | 20.54 us | 181.6406 | 181.6406 | 181.6406 | 781.4 KB |
Parse net6.0
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|
| .NET Default Parse(String) | 7.005 ms | 0.1261 ms | 0.2071 ms | 78.1250 | 78.1250 | 78.1250 | 390.81 KB |
| Home brewed Parse(String) | 3.833 ms | 0.0741 ms | 0.0854 ms | 101.5625 | 101.5625 | 101.5625 | 390.73 KB |
| FastEnum Parse(String) | 2.724 ms | 0.0542 ms | 0.0580 ms | 101.5625 | 101.5625 | 101.5625 | 390.73 KB |
| Enums.NET Parse(String) | 3.073 ms | 0.0356 ms | 0.0333 ms | 101.5625 | 101.5625 | 101.5625 | 390.73 KB |
| .NET Default Parse(Span) | 6.840 ms | 0.0797 ms | 0.0745 ms | 93.7500 | 93.7500 | 93.7500 | 390.81 KB |
| Home brewed Parse(Span.ToString()) | 4.129 ms | 0.0821 ms | 0.1124 ms | 93.7500 | 93.7500 | 93.7500 | 390.73 KB |
| FastEnum Parse(Span.ToString()) | 3.036 ms | 0.0596 ms | 0.0586 ms | 101.5625 | 101.5625 | 101.5625 | 390.73 KB |
| Enums.NET Parse(Span.ToString()) | 3.109 ms | 0.0482 ms | 0.0451 ms | 101.5625 | 101.5625 | 101.5625 | 390.76 KB |
Parse net7.0
| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|
| .NET Default Parse(String) | 5.533 ms | 0.1095 ms | 0.1499 ms | 93.7500 | 93.7500 | 93.7500 | 390.82 KB |
| Home brewed Parse(String) | 3.406 ms | 0.0519 ms | 0.0460 ms | 39.0625 | 39.0625 | 39.0625 | 390.71 KB |
| FastEnum Parse(String) | 2.224 ms | 0.0355 ms | 0.0315 ms | 39.0625 | 39.0625 | 39.0625 | 390.71 KB |
| Enums.NET Parse(String) | 2.848 ms | 0.0545 ms | 0.0535 ms | 101.5625 | 101.5625 | 101.5625 | 390.73 KB |
| .NET Default Parse(Span) | 5.367 ms | 0.0475 ms | 0.0421 ms | 39.0625 | 39.0625 | 39.0625 | 390.75 KB |
| Home brewed Parse(Span.ToString()) | 3.547 ms | 0.0369 ms | 0.0345 ms | 39.0625 | 39.0625 | 39.0625 | 390.71 KB |
| FastEnum Parse(Span.ToString()) | 2.397 ms | 0.0264 ms | 0.0247 ms | 39.0625 | 39.0625 | 39.0625 | 390.71 KB |
| Enums.NET Parse(Span.ToString()) | 3.416 ms | 0.0682 ms | 0.0911 ms | 101.5625 | 101.5625 | 101.5625 | 390.73 KB |
Analysis
The problem almost certainly in not enough caching. All of these different solutions revolve around using reflection at runtime to look up the values of these values. The faster solutions generally use more caching to speed up performance. I'd suggest the .NET implementation either take a more aggressive approach to caching and or potentially look into using source generators to negate the startup cost associated with the caching altogether.