Skip to content

Add Generic Enum.TryFormat() - #71590

Closed
heathbm wants to merge 6 commits into
dotnet:mainfrom
heathbm:implement-enum-tryformat
Closed

Add Generic Enum.TryFormat()#71590
heathbm wants to merge 6 commits into
dotnet:mainfrom
heathbm:implement-enum-tryformat

Conversation

@heathbm

@heathbmheathbm commented Jul 3, 2022

Copy link
Copy Markdown
Contributor

Partial implementation of #57881

Benchmarks

Code: https://gist.github.com/heathbm/59783b3d9e08d9556d7a86fa6ee2cd20

Before implementation:

MethodMeanErrorStdDevMedianMinMaxGen 0Allocated
FormatG33.96 ns0.722 ns0.676 ns33.80 ns33.07 ns35.16 ns0.001924 B
FormatD27.03 ns0.149 ns0.132 ns27.00 ns26.84 ns27.29 ns0.001924 B
FormatX35.46 ns0.737 ns0.789 ns35.42 ns33.99 ns36.74 ns0.005064 B
FormatF33.69 ns0.270 ns0.225 ns33.73 ns33.17 ns33.95 ns0.001824 B

After implementation:

MethodMeanErrorStdDevMedianMinMaxGen 0Allocated
FormatG32.983 ns0.2221 ns0.1969 ns32.980 ns32.588 ns33.40 ns0.001824 B
TryFormatG14.332 ns0.1794 ns0.1678 ns14.412 ns13.943 ns14.51 ns--
FormatD26.751 ns0.8298 ns0.9556 ns26.348 ns25.166 ns28.47 ns0.001924 B
TryFormatD9.906 ns0.1853 ns0.1734 ns9.862 ns9.747 ns10.35 ns--
FormatX34.408 ns0.2204 ns0.1954 ns34.430 ns33.866 ns34.63 ns0.005064 B
TryFormatX17.993 ns0.0465 ns0.0435 ns17.998 ns17.913 ns18.08 ns--
FormatF34.604 ns0.2278 ns0.2131 ns34.486 ns34.385 ns34.99 ns0.001824 B
TryFormatF17.251 ns0.0471 ns0.0368 ns17.247 ns17.172 ns17.31 ns--

No visible regression.

Profiling:

Profiling code: https://gist.github.com/heathbm/079c4bea1dc541ffc12c91f283892875
Tested with:

  • Visual Studio Performance Profiler (.NET Object Allocation Tracking)
  • DotMemory (Full, Entire process tree)

Only allocations appear in the first run from:

  • System.Enum.GetEnumInfo
  • System.RuntimeType.GetTypeCodeImpl
    I believe there is nothing I can do to prevent these allocations on the first run as they generate cache.

alloc1
alloc2

@ghost

ghost commented Jul 3, 2022

Copy link
Copy Markdown

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghostghost added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2022
@ghost

ghost commented Jul 3, 2022

Copy link
Copy Markdown

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

@ghost

ghost commented Jul 3, 2022

Copy link
Copy Markdown

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

Issue Details

Partial implementation of #57881

Benchmarks

usingBenchmarkDotNet.Attributes;usingMicroBenchmarks;namespaceSystem.Tests{[MemoryDiagnoser][BenchmarkCategory(Categories.Libraries)]publicclassPerf_Enum{publicenumColors{Red=0x1,Orange=0x2,Yellow=0x4,Green=0x8,Blue=0x10}[Benchmark]publicvoidFormat(){vardestination=Enum.Format(typeof(Colors),Colors.Green,"F");}[Benchmark]publicvoidTryFormat(){Span<char>destination=stackallocchar[5];Enum.TryFormat(Colors.Green,destination,out_,"F");}}}
MethodMeanErrorStdDevMedianMinMaxGen 0Allocated
Format31.90 ns0.359 ns0.318 ns31.94 ns31.04 ns32.28 ns0.001924 B
TryFormat14.96 ns0.053 ns0.047 ns14.95 ns14.88 ns15.05 ns--

Profiling:

Profiling code: https://gist.github.com/heathbm/079c4bea1dc541ffc12c91f283892875
Tested with:

  • Visual Studio Performance Profiler (.NET Object Allocation Tracking)
  • DotMemory (Full, Entire process tree)

Only allocations appear in the first run from:

  • System.Enum.GetEnumInfo
  • System.RuntimeType.GetTypeCodeImpl
    I believe there is nothing I can do to prevent these allocations on the first run as they generate cache.

alloc1
alloc2

Author:heathbm
Assignees:-
Labels:

area-System.Runtime, new-api-needs-documentation, community-contribution

Milestone:-

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated

@deeprobindeeprobin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks overall good to me

Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated
Comment threadsrc/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

The Enum tests are failing.

@heathbm

heathbm commented Jul 13, 2022

Copy link
Copy Markdown
ContributorAuthor

The Enum tests are failing.

@jkotas I have removed a Debug.Assert that was unreliable since that assert can now deal with externally provided spans, that can be larger than what was originally expected. I did not see any way to elegantly satisfy the assert without additional operations just to make the assert pass.

@danmoseley

Copy link
Copy Markdown
Contributor

@jkotas was your feedback addressed?

@jkotas

Copy link
Copy Markdown
Member

It was addressed, thought there is still pretty significant code-bloat per instantiations that could be avoided by more significant refactoring.

Are there places in this repo where this method can be used?

@stephentoub

Copy link
Copy Markdown
Member

Are there places in this repo where this method can be used?

My expectation is we'll use it at a minimum in our interpolated string handlers, to avoid the boxing and string allocations that come from falling back to using ToString today. We can start by calling this explicitly after doing some type testing (e.g. typeof(T).IsEnum when it's an intrinsic), though with the constraints on the public signature we'll probably need to move the logic out into an unconstrained helper. Then that special-casing can be removed if/when Enum implements ISpanFormattable and the JIT special-cases it, though at that point I expect this implementation would be used by that ISpanFormattable implementation.

@dakersnardakersnar added the needs-author-action An issue or pull request that requires more info or actions from the author. label Oct 10, 2022
@dakersnar

Copy link
Copy Markdown
Contributor

@jkotas I added the "needs-author-action" label until your code-bloat comment is addressed.

@ghost

Copy link
Copy Markdown

This pull request 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

ghost commented Nov 8, 2022

Copy link
Copy Markdown

This pull request 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 pull request, but please note that it will be locked if it remains inactive for another 30 days.

@ghostghost closed this Nov 8, 2022
@dakersnar

Copy link
Copy Markdown
Contributor

Discussed this with @jkotas, paraphrasing our conversation here to let others weigh in. Reopening for now.

To address his code bloat comment, we would need something like #76398. He does not have a strong opinion on whether or not we should merge this in its current state, but we will likely have some perf regressions in existing APIs if we do.

@dakersnardakersnar reopened this Nov 8, 2022
@dakersnardakersnar added blocked Issue/PR is blocked on something - see comments and removed no-recent-activity labels Nov 8, 2022
@stephentoub

Copy link
Copy Markdown
Member

we will likely have some perf regressions in existing APIs if we do

What kind of regressions, and do you have an example of where we'd expect this?

@jkotas

Copy link
Copy Markdown
Member

What kind of regressions, and do you have an example of where we'd expect this?

There seems to be some extra work done on paths through the existing APIs. We should run enum microbenchmarks to see whether it is going to show up.

@stephentoub

stephentoub commented Nov 8, 2022

Copy link
Copy Markdown
Member

There seems to be some extra work done on paths through the existing APIs.

I see. It seems like that was done in response to feedback that there was logical code duplication, and the changes are due to consolidating it?

It'd be really nice to get TryFormat added. Would you prefer it be entirely new code, without touching existing code paths, and then subsequently we can look at how to unify as much as possible without regression?

We should run enum microbenchmarks to see whether it is going to show up.

👍

@jkotas

Copy link
Copy Markdown
Member

It'd be really nice to get TryFormat added. Would you prefer it be entirely new code, without touching existing code paths, and then subsequently we can look at how to unify as much as possible without regression?

Ok with me. I am not happy about the amount of code bloat generated for each use of the generic enums parse and format methods. It is a pre-existing problem.

@stephentoub

Copy link
Copy Markdown
Member

I've assigned this to myself to land it.

@stephentoubstephentoub removed blocked Issue/PR is blocked on something - see comments needs-author-action An issue or pull request that requires more info or actions from the author. labels Nov 15, 2022
@stephentoub
stephentoubforce-pushed the implement-enum-tryformat branch from bc25940 to 8299b28CompareNovember 15, 2022 04:25
- Add TryFormat XML docs
- Make TryFormat's format parameter optional and allow an empty specifier
- Make it an exception to specify invalid format specifier in TryFormat
- Streamline TryFormat's default specifier case
- Add missing cases where feasible for char/bool/nint/nuint underlying types
- Reduce duplicate branches in hex formatting methods where e.g. int/uint cases can be shared
- Refactor method implementations to better share return blocks
- Add AggressiveInlining to places where call sites are finite / overhead is measurable
- Remove checked multiplication from length calculation
- Remove some branching from code to find name(s) in flags names
- Revise multi-flag name writing to reduce bounds checking
- Change all format specifier switches to use ASCII casing trick
- Outline exception creation from generic methods to reduce specialized code bloat
- Rename helper methods to make their functionality clearer
- Tweak formatting for consistency within the file
- Use Enum.GetUnderlyingType instead of Type.GetTypeCode so that the intrinsic enables optimization at the call site
- Add a generic cache with readonly statics that enables more generic specialization and branch elimination at call sites
- Add more missing nint/nuint/char/etc. cases
- Add a generic GetEnumName that can trim away half based on ValuesAreSequentialFromZero, that can get a hardcoded names address (once the frozen work is complete), and that can optimize out a branch in the inlined FindDefinedIndex
- Remove a bounds check from GetEnumName on the find path
- Expose an internal TryFormatUnconstrained without Enum/struct constraints for corelib interpolated string handlers to use
@stephentoub
stephentoubforce-pushed the implement-enum-tryformat branch from 8299b28 to f993036CompareNovember 15, 2022 05:03
@stephentoub

Copy link
Copy Markdown
Member

I've made a bunch of changes here, in additional commits (I squashed all the existing commits down to one). As part of validating it, I fixed #76157. This also contributes to #76398, the main missing piece there being different types for the values array. And I realize the GenericEnumInfo<TEnum> cache might be controversial, but while it adds a new generically-specialized class per enum, it also reduces the amount of code in various generic methods by eliminating unnecessary code paths... @jkotas?

I have a change to push up to dotnet/performance to add more tests. The existing non-generic methods effectively remain unchanged throughput-wise, subject to noise, or get a bit better. The tests:

[Benchmark][Arguments(Colors.Yellow)][Arguments(Colors.Yellow|Colors.Blue)][Arguments(Colors.Red|Colors.Orange|Colors.Yellow|Colors.Green|Colors.Blue)][Arguments(Colors.Yellow|(Colors)0x20)][Arguments(0x20)]publicstringEnumToString(Colorsvalue)=>value.ToString();[Benchmark][Arguments(SearchOption.TopDirectoryOnly)][Arguments(SearchOption.AllDirectories)][Arguments((SearchOption)(-1))]publicstringEnumToString_SmallNonFlagsEnum(SearchOptionvalue)=>value.ToString();[Benchmark][Arguments(UnicodeCategory.UppercaseLetter)][Arguments(UnicodeCategory.Control)][Arguments(UnicodeCategory.Format)][Arguments(UnicodeCategory.OtherNotAssigned)][Arguments((UnicodeCategory)42)]publicstringEnumToString_LargeNonFlagsEnum(UnicodeCategoryvalue)=>value.ToString();[Benchmark][Arguments(DayOfWeek.Sunday,"")][Arguments(DayOfWeek.Monday,"g")][Arguments(DayOfWeek.Tuesday,"d")][Arguments(DayOfWeek.Wednesday,"x")][Arguments(DayOfWeek.Thursday,"f")][Arguments(DayOfWeek.Friday,"X")][Arguments(DayOfWeek.Saturday,"D")][Arguments((DayOfWeek)7,"G")][Arguments((DayOfWeek)8,"F")]publicstringEnumToStringWithFormat(DayOfWeekvalue,stringformat)=>value.ToString(format);

on my machine result in:

MethodJobToolchainvalueformatMeanErrorStdDevMedianMinMaxRatioRatioSDGen0AllocatedAlloc Ratio
EnumToString_SmallNonFlagsEnumJob-EUILUD\main\corerun.exe-1?30.90 ns0.842 ns0.935 ns30.85 ns29.520 ns32.73 ns1.000.000.008956 B1.00
EnumToString_SmallNonFlagsEnumJob-HGADFP\pr\corerun.exe-1?30.17 ns0.354 ns0.331 ns30.10 ns29.722 ns30.83 ns0.980.040.008956 B1.00
EnumToStringJob-EUILUD\main\corerun.exe32?30.64 ns0.626 ns0.586 ns30.52 ns29.962 ns31.66 ns1.000.000.008956 B1.00
EnumToStringJob-HGADFP\pr\corerun.exe32?29.51 ns0.641 ns0.569 ns29.41 ns28.576 ns30.68 ns0.960.020.008956 B1.00
EnumToStringJob-EUILUD\main\corerun.exe36?31.45 ns0.756 ns0.841 ns31.40 ns30.093 ns32.96 ns1.000.000.008856 B1.00
EnumToStringJob-HGADFP\pr\corerun.exe36?30.92 ns0.697 ns0.802 ns30.94 ns29.512 ns32.16 ns0.980.030.008956 B1.00
EnumToString_LargeNonFlagsEnumJob-EUILUD\main\corerun.exe42?23.27 ns0.948 ns1.054 ns23.21 ns21.876 ns25.69 ns1.000.000.008956 B1.00
EnumToString_LargeNonFlagsEnumJob-HGADFP\pr\corerun.exe42?22.20 ns0.886 ns1.020 ns21.97 ns20.722 ns24.58 ns0.950.060.008856 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeSunday12.65 ns0.597 ns0.688 ns12.45 ns11.782 ns14.28 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeSunday13.14 ns0.573 ns0.636 ns13.20 ns12.169 ns14.19 ns1.050.080.003824 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeMondayg13.78 ns0.327 ns0.321 ns13.80 ns13.160 ns14.46 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeMondayg14.35 ns0.629 ns0.725 ns14.15 ns13.465 ns15.91 ns1.050.070.003824 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeTuesdayd10.27 ns0.324 ns0.373 ns10.25 ns9.808 ns11.09 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeTuesdayd10.67 ns0.500 ns0.535 ns10.67 ns10.003 ns11.55 ns1.040.060.003824 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeWednesdayx24.40 ns0.604 ns0.696 ns24.28 ns23.485 ns25.72 ns1.000.000.010164 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeWednesdayx23.63 ns0.895 ns0.994 ns23.24 ns22.779 ns26.06 ns0.970.050.010164 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeThursdayf19.31 ns0.663 ns0.737 ns19.25 ns18.538 ns20.99 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeThursdayf18.21 ns0.521 ns0.600 ns18.01 ns17.386 ns19.39 ns0.940.040.003824 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeFridayX24.42 ns0.529 ns0.544 ns24.30 ns23.235 ns25.43 ns1.000.000.010164 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeFridayX24.39 ns1.094 ns1.260 ns24.11 ns22.857 ns27.07 ns1.010.060.010264 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exeSaturdayD10.26 ns0.349 ns0.388 ns10.17 ns9.736 ns10.99 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exeSaturdayD10.43 ns0.236 ns0.197 ns10.43 ns10.102 ns10.80 ns1.030.040.003824 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exe7G16.72 ns0.866 ns0.998 ns16.46 ns15.390 ns19.04 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exe7G16.91 ns0.688 ns0.792 ns16.72 ns15.934 ns18.58 ns1.020.090.003824 B1.00
EnumToStringWithFormatJob-EUILUD\main\corerun.exe8F27.46 ns1.296 ns1.492 ns27.25 ns25.967 ns30.82 ns1.000.000.003824 B1.00
EnumToStringWithFormatJob-HGADFP\pr\corerun.exe8F24.61 ns0.537 ns0.528 ns24.78 ns23.632 ns25.43 ns0.890.050.003824 B1.00
EnumToStringJob-EUILUD\main\corerun.exeRed, Orange, Yellow, Green, Blue?56.09 ns2.247 ns2.497 ns55.84 ns52.697 ns62.04 ns1.000.000.0176112 B1.00
EnumToStringJob-HGADFP\pr\corerun.exeRed, Orange, Yellow, Green, Blue?50.89 ns1.480 ns1.704 ns51.20 ns48.498 ns54.65 ns0.910.050.0177112 B1.00
EnumToString_LargeNonFlagsEnumJob-EUILUD\main\corerun.exeUppercaseLetter?12.61 ns0.408 ns0.470 ns12.54 ns11.753 ns13.31 ns1.000.000.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-HGADFP\pr\corerun.exeUppercaseLetter?12.07 ns0.296 ns0.304 ns12.04 ns11.664 ns12.77 ns0.960.050.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-EUILUD\main\corerun.exeControl?11.62 ns0.211 ns0.176 ns11.60 ns11.412 ns12.05 ns1.000.000.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-HGADFP\pr\corerun.exeControl?11.09 ns0.185 ns0.164 ns11.08 ns10.898 ns11.41 ns0.950.020.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-EUILUD\main\corerun.exeFormat?12.32 ns0.266 ns0.236 ns12.25 ns12.059 ns12.80 ns1.000.000.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-HGADFP\pr\corerun.exeFormat?12.32 ns0.360 ns0.400 ns12.24 ns11.825 ns13.30 ns1.000.040.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-EUILUD\main\corerun.exeOtherNotAssigned?12.44 ns0.429 ns0.494 ns12.38 ns11.765 ns13.54 ns1.000.000.003824 B1.00
EnumToString_LargeNonFlagsEnumJob-HGADFP\pr\corerun.exeOtherNotAssigned?12.19 ns0.504 ns0.580 ns12.10 ns11.369 ns13.23 ns0.980.060.003824 B1.00
EnumToString_SmallNonFlagsEnumJob-EUILUD\main\corerun.exeTopDirectoryOnly?12.71 ns0.590 ns0.679 ns12.50 ns11.860 ns13.87 ns1.000.000.003824 B1.00
EnumToString_SmallNonFlagsEnumJob-HGADFP\pr\corerun.exeTopDirectoryOnly?11.58 ns0.292 ns0.312 ns11.56 ns11.258 ns12.37 ns0.920.050.003824 B1.00
EnumToString_SmallNonFlagsEnumJob-EUILUD\main\corerun.exeAllDirectories?12.41 ns0.389 ns0.448 ns12.23 ns11.899 ns13.42 ns1.000.000.003824 B1.00
EnumToString_SmallNonFlagsEnumJob-HGADFP\pr\corerun.exeAllDirectories?11.72 ns0.272 ns0.212 ns11.74 ns11.248 ns11.98 ns0.950.030.003824 B1.00
EnumToStringJob-EUILUD\main\corerun.exeYellow?16.38 ns0.561 ns0.623 ns16.10 ns15.764 ns17.78 ns1.000.000.003824 B1.00
EnumToStringJob-HGADFP\pr\corerun.exeYellow?15.57 ns0.349 ns0.292 ns15.61 ns14.937 ns16.00 ns0.960.040.003824 B1.00
EnumToStringJob-EUILUD\main\corerun.exeYellow, Blue?36.05 ns1.333 ns1.535 ns35.79 ns33.932 ns39.00 ns1.000.000.011372 B1.00
EnumToStringJob-HGADFP\pr\corerun.exeYellow, Blue?35.32 ns1.559 ns1.795 ns35.30 ns32.570 ns38.40 ns0.980.040.011472 B1.00

For TryFormat perf, the impact is directly visible via interpolated strings, which now use Enum.TryFormat:

[Benchmark][Arguments(Colors.Red|Colors.Green)][Arguments(0x20)]publicboolInterpolateEnum(Colorsvalue)=>MemoryExtensions.TryWrite(s_scratch,$"{value}{value:g}{value:d}{value:x}{value:f}",out_);

results in:

MethodJobToolchainvalueMeanErrorStdDevMedianMinMaxRatioRatioSDGen0AllocatedAlloc Ratio
InterpolateEnumJob-GEPFJR\main\corerun.exe32158.75 ns3.920 ns4.357 ns156.08 ns155.11 ns171.78 ns1.000.000.0458288 B1.00
InterpolateEnumJob-ABMNOH\pr\corerun.exe3296.27 ns1.065 ns0.889 ns96.10 ns95.37 ns97.89 ns0.600.02--0.00
InterpolateEnumJob-GEPFJR\main\corerun.exeRed, Green173.81 ns4.712 ns5.238 ns173.31 ns164.44 ns182.48 ns1.000.000.0480304 B1.00
InterpolateEnumJob-ABMNOH\pr\corerun.exeRed, Green113.90 ns3.000 ns3.334 ns113.61 ns108.63 ns120.92 ns0.660.03--0.00

Other generic Enum methods also improve, e.g.

privateColors_colorValue=Colors.Blue;privateDayOfWeek_dayOfWeekValue=DayOfWeek.Saturday;[Benchmark]publicboolIsDefined()=>Enum.IsDefined(_colorValue);[Benchmark]publicboolIsDefined_NonFlags()=>Enum.IsDefined(_dayOfWeekValue);[Benchmark]publicstringGetName()=>Enum.GetName(_colorValue);[Benchmark]publicstringGetName_NonFlags()=>Enum.GetName(_dayOfWeekValue);[Benchmark]publicstring[]GetNames()=>Enum.GetNames<Colors>();

results in:

MethodJobToolchainMeanErrorStdDevMedianMinMaxRatioRatioSDGen0AllocatedAlloc Ratio
IsDefinedJob-UKYMDD\main\corerun.exe6.572 ns0.1094 ns0.0854 ns6.559 ns6.471 ns6.715 ns1.000.00--NA
IsDefinedJob-RHTFZD\pr\corerun.exe3.830 ns0.0989 ns0.1100 ns3.773 ns3.728 ns4.091 ns0.580.02--NA
IsDefined_NonFlagsJob-UKYMDD\main\corerun.exe3.751 ns0.0670 ns0.0627 ns3.762 ns3.652 ns3.855 ns1.000.00--NA
IsDefined_NonFlagsJob-RHTFZD\pr\corerun.exe1.407 ns0.0512 ns0.0479 ns1.396 ns1.357 ns1.506 ns0.380.01--NA
GetNameJob-UKYMDD\main\corerun.exe9.214 ns0.5445 ns0.6271 ns9.119 ns8.359 ns10.771 ns1.000.00--NA
GetNameJob-RHTFZD\pr\corerun.exe5.003 ns0.1648 ns0.1832 ns5.017 ns4.672 ns5.262 ns0.540.04--NA
GetName_NonFlagsJob-UKYMDD\main\corerun.exe4.945 ns0.2827 ns0.3255 ns4.914 ns4.603 ns5.786 ns1.000.00--NA
GetName_NonFlagsJob-RHTFZD\pr\corerun.exe2.499 ns0.1435 ns0.1653 ns2.509 ns2.311 ns2.816 ns0.510.05--NA
GetNamesJob-UKYMDD\main\corerun.exe15.713 ns0.7786 ns0.8966 ns15.475 ns14.753 ns17.493 ns1.000.000.010164 B1.00
GetNamesJob-RHTFZD\pr\corerun.exe14.534 ns0.3457 ns0.3065 ns14.432 ns14.123 ns15.052 ns0.930.070.010164 B1.00

[Serializable]
[System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public abstract partial class Enum : ValueType, IComparable, IFormattable, IConvertible

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure this is deliberate, but why not implement ISpanFormattable?

@stephentoubstephentoubNov 15, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every call to TryFormat would box the enum, just as every call to ToString() today boxes the enum. To avoid that would require non-trivial work in the JIT to rewrite calls from the instance method to something else. Unlike GetHashCode and HasFlags, which are special-cased, the implementation of formatting is complicated and not something we'd want to replicate in the JIT itself, so most likely we'd need a static generic TryFormat anyway the JIT could rewrite the calls to target. And we don't want to expose an ISpanFormattable implementation that's going to implicitly have such expense unless/until it can be eliminated.

@driesengdriesengNov 16, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub thanks for taking the time to respond, I really appreciate it! Why can't you use constrained calls with enums to avoid boxing (like what's done here, for example)?

PS. Last question, I promise :p

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Try just running:

for(inti=0;i<1000;i++)DayOfWeek.Monday.ToString();

under an allocation profiler. You'll see it allocates 1000 enum objects. The implementation of ToString itself is defined on the System.Enum reference type.

@jkotas

Copy link
Copy Markdown
Member

it also reduces the amount of code in various generic methods by eliminating unnecessary code paths..

There is still a very non-trivial code duplication per enum specialization. Here are some numbers for a simple enum with 3 values. The code that I have used to get these numbers is here: https://gist.github.com/jkotas/62f1680635e230a44bad2027ace5effa

TryFormat specialization in this PR costs about 7.5kB of workingset footprint per enum. It feels like a lot. For comparison, non-generic Enum.ToString costs about 550 bytes per enum. A simple source-generated ToString method costs 680 bytes per enum (this is tier 0 cost, it is down to 439 bytes with tiered compilation disabled).

To get the per-enum instantiation overhead down, I think the implementation would need to look like this:

// This is the only method that is duplicated per enum instantiation[MethodImpl(MethodImplOptions.AggressiveInlining)]staticboolTryFormat<TEnum>(TEnumvalue,Span<char>destination,outintcharsWritten,[StringSyntax(StringSyntaxAttribute.EnumFormat)]ReadOnlySpan<char>format=default)whereTEnum:struct,Enum{EnumInfoenumInfo=GetEnumInfo(typeof(TEnum));// We may want to tweak the JIT so that it dead-code eliminates unreachable branches even in tier 0TypeunderlyingType=typeof(TEnum).GetEnumUnderlyingType();if(underlyingType==typeof(int))returnTryFormatPrimitive<int>(Unsafe.As<TEnum,int>(refvalue),destination,charsWritten,format,enumInfo);if(underlyingType==typeof(long))returnTryFormatPrimitive<long>(Unsafe.As<TEnum,long>(refvalue),destination,charsWritten,format,enumInfo);
...}// This method is only instantiated per finite set of primitive types.staticboolTryFormatPrimitive<TUnderlyingType>(TUnderlyingTypevalue,Span<char>destination,outintcharsWritten,ReadOnlySpan<char>format,EnumInfoenumInfo)whereTUnderlyingType:ISpanFormattable{
...}

@stephentoub

Copy link
Copy Markdown
Member

There is still a very non-trivial code duplication per enum specialization

Which is why this doesn't close #76398.

But, fine. I'll close this and iterate on it some more locally rather than doing it subsequently. @heathbm, I'll preserve your original commit in any subsequent PR I put up.

@stephentoub

Copy link
Copy Markdown
Member

// We may want to tweak the JIT so that it dead-code eliminates unreachable branches even in tier 0

@EgorBo

@EgorBo

Copy link
Copy Markdown
Member

Reference in new

Thanks, will add it to the list of potential improvements for tier0

@ghostghost locked as resolved and limited conversation to collaborators Dec 17, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtimecommunity-contributionIndicates that the PR has been added by a community membernew-api-needs-documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

11 participants

@heathbm@jkotas@danmoseley@stephentoub@dakersnar@EgorBo@drieseng@deeprobin@tannergooding@teo-tsirpanis@am11