Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Enum function performance (TryParse, ToString, Parse, etc...) #79762

Closed
tkreindler opened this issue Dec 16, 2022 · 6 comments
Closed
Labels

Comments

@tkreindler
Copy link

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.

@tkreindler tkreindler added the tenet-performance Performance related issue label Dec 16, 2022
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Dec 16, 2022
@jkotas
Copy link
Member

jkotas commented Dec 16, 2022

Could you please share sources for your benchmarks?

Note that .NET 8 has some enum performance improvements already (e.g. #78580).

@ghost
Copy link

ghost commented Dec 17, 2022

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

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.

Author: tkreindler
Assignees: -
Labels:

area-System.Runtime, tenet-performance, untriaged

Milestone: -

@dakersnar dakersnar added the needs-author-action An issue or pull request that requires more info or actions from the author. label Dec 19, 2022
@ghost
Copy link

ghost commented Dec 19, 2022

This issue has been marked needs-author-action and may be missing some important information.

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Dec 19, 2022
@ghost ghost added the no-recent-activity label Jan 2, 2023
@ghost
Copy link

ghost commented Jan 2, 2023

This issue has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

@ghost
Copy link

ghost commented Jan 17, 2023

This issue will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the issue, but please note that the issue will be locked if it remains inactive for another 30 days.

@ghost ghost closed this as completed Jan 17, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Feb 16, 2023
@tannergooding tannergooding removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 24, 2024
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

4 participants