Skip to content

Rewrite Enum and add {ISpanFormattable}.TryFormat - #78580

Merged
stephentoub merged 15 commits into
dotnet:mainfrom
stephentoub:enumtryformat
Dec 6, 2022
Merged

Rewrite Enum and add {ISpanFormattable}.TryFormat#78580
stephentoub merged 15 commits into
dotnet:mainfrom
stephentoub:enumtryformat

Conversation

@stephentoub

@stephentoubstephentoub commented Nov 18, 2022

Copy link
Copy Markdown
Member

Fixes#57881
Fixes#76157
Fixes#76398
Fixes#29266

This rewrites Enum to change how it stores the values data. Rather than having a non-generic EnumInfo that stores a ulong[] array of all values, there’s a generic EnumInfo that stores a TUnderlyingType[]. Then based on the enum’s type, every entry point maps to an underlying TUnderlyingType and invokes a generic method with that TUnderlyingType, e.g. Enum.IsDefined(…) and Enum.IsDefined(typeof(TEnum), …) will look up the TUnderlyingValue and then invoke Enum.IsDefinedPrimitive(typeof(TEnum)). In this way, a) we store an array strongly typed to the underlying value rather than storing the worst case ulong[], and b) we share implementations across generic and non-generic entrypoints while not having full generic specialization for every TEnum; worst case, we have only one generic specialization per underlying type, of which only 8 are expressible in C#. The generic entrypoints are able to do the mapping very efficiently, thanks to the recently added enum-based intrinsics. The non-generic entrypoints use the same switches on TypeCode/CorElementType they do today when doing e.g. ToUInt64.

@jkotas, I hope this meets your desires as expressed in #71590 (comment). If I've misinterpreted or you were hoping for something else, please let me know. There, you quoted the TryFormat method increasing working set by 7.5kb, which I was able to repro. With this PR, it's ~3.5kb with tiered compilation off... with tiered compilation enables, it's actually more, at ~9.5kb. Part of the issue appears to be how the repro itself is structured and how it interacts with JIT inlining, which is to say, not well; the JIT gives up inlining very early on, and that looks to contribute meaningfully to the size (plus little optimization happening in tier 0, hence the large number).

This also adds the static Enum.TryFormat as well as changing Enum to implement ISpanFormattable, with ISpanFormattable.TryFormat as an explicit implementation. These both share the same underlying TUnderlyingValue-based generic implementation, with just the entrypoint methods differing as to how the TUnderlyingValue mapping is done. And then that Enum.TryFormat is used in multiple interpolated string handlers to avoid boxing the enum; today it’ll be boxed as part of using its IFormattable implementation, and with this change, it both won’t be boxed and will call through the optimized generic path. This uses typeof(T).IsEnum, which is now a JIT intrinsic that’ll become a const true/false.

Overall, performance improves, in particular for the generic methods. Some of the non-generics get better as well. However, there is a repeatable regression here for other non-generic methods, in particular the non-generic Parse methods, and I haven’t been able to root cause that yet. @jkotas or @EgorBo, if you see anything obvious here, please let me know.

This includes the initial commit from #71590, although the primary thing still around from that are the tests that were added. Thanks, @heathbm.

@tannergooding and/or @dakersnar, if you could help look at the use of generic math, that'd be welcome.

I have a set of changes I plan to PR to dotnet/performance. Here are the results of those tests locally:

MethodToolchainvalueformattextMeanRatioCode SizeAllocatedAlloc Ratio
HasFlag\main\corerun.exe???0.0000 ns?12 B-?
HasFlag\pr\corerun.exe???0.0119 ns?12 B-?
Compare\main\corerun.exe???0.0004 ns?11 B-?
Compare\pr\corerun.exe???0.0142 ns?11 B-?
GetName_NonGeneric_Flags\main\corerun.exe???24.5866 ns1.00430 B24 B1.00
GetName_NonGeneric_Flags\pr\corerun.exe???36.9405 ns1.50235 B24 B1.00
IsDefined_Generic_Flags\main\corerun.exe???6.4346 ns1.00202 B-NA
IsDefined_Generic_Flags\pr\corerun.exe???5.4740 ns0.85194 B-NA
IsDefined_Generic_NonFlags\main\corerun.exe???3.5333 ns1.00202 B-NA
IsDefined_Generic_NonFlags\pr\corerun.exe???1.7395 ns0.49194 B-NA
GetName_Generic_Flags\main\corerun.exe???8.4323 ns1.001,298 B-NA
GetName_Generic_Flags\pr\corerun.exe???6.9253 ns0.82625 B-NA
GetName_Generic_NonFlags\main\corerun.exe???5.2012 ns1.00763 B-NA
GetName_Generic_NonFlags\pr\corerun.exe???3.6108 ns0.69625 B-NA
GetNames_Generic\main\corerun.exe???14.5402 ns1.00231 B64 B1.00
GetNames_Generic\pr\corerun.exe???14.0541 ns0.96241 B64 B1.00
GetValues_Generic\main\corerun.exe???349.5134 ns1.0069 B168 B1.00
GetValues_Generic\pr\corerun.exe???11.0484 ns0.03163 B48 B0.29
GetValuesAsUnderlyingType_Generic\main\corerun.exe???34.9357 ns1.00783 B144 B1.00
GetValuesAsUnderlyingType_Generic\pr\corerun.exe???16.9793 ns0.49215 B144 B1.00
GetValuesAsUnderlyingType_NonGeneric\main\corerun.exe???34.8806 ns1.0073 B144 B1.00
GetValuesAsUnderlyingType_NonGeneric\pr\corerun.exe???24.0148 ns0.6973 B144 B1.00
ToString_NonFlags_Small\main\corerun.exe-1??30.4135 ns1.00313 B56 B1.00
ToString_NonFlags_Small\pr\corerun.exe-1??27.6264 ns0.91665 B56 B1.00
ToString_Flags\main\corerun.exe32??29.4235 ns1.00313 B56 B1.00
ToString_Flags\pr\corerun.exe32??25.2869 ns0.87665 B56 B1.00
StringFormat\main\corerun.exe32??338.9915 ns1.00331 B352 B1.00
StringFormat\pr\corerun.exe32??245.3310 ns0.72331 B88 B0.25
InterpolateIntoString\main\corerun.exe32??244.6292 ns1.001,890 B352 B1.00
InterpolateIntoString\pr\corerun.exe32??130.7233 ns0.542,068 B64 B0.18
InterpolateIntoSpan_Flags\main\corerun.exe32??169.0338 ns1.00492 B288 B1.00
InterpolateIntoSpan_Flags\pr\corerun.exe32??90.0121 ns0.55492 B-0.00
InterpolateIntoStringBuilder_Flags\main\corerun.exe32??214.4654 ns1.00286 B288 B1.00
InterpolateIntoStringBuilder_Flags\pr\corerun.exe32??110.9654 ns0.53286 B24 B0.08
ToString_Flags\main\corerun.exe36??29.8668 ns1.00313 B56 B1.00
ToString_Flags\pr\corerun.exe36??25.2438 ns0.85665 B56 B1.00
ToString_NonFlags_Large\main\corerun.exe42??21.9036 ns1.00313 B56 B1.00
ToString_NonFlags_Large\pr\corerun.exe42??17.2329 ns0.79665 B56 B1.00
InterpolateIntoSpan_NonFlags\main\corerun.exe42??165.0200 ns1.00492 B288 B1.00
InterpolateIntoSpan_NonFlags\pr\corerun.exe42??94.0216 ns0.57492 B-0.00
InterpolateIntoStringBuilder_NonFlags\main\corerun.exe42??177.5868 ns1.00286 B288 B1.00
InterpolateIntoStringBuilder_NonFlags\pr\corerun.exe42??115.2136 ns0.65286 B24 B0.08
ToString_Format_NonFlags\main\corerun.exe7G?15.0427 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exe7G?11.5376 ns0.771,444 B24 B1.00
ToString_Format_NonFlags\main\corerun.exe8F?25.9959 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exe8F?23.0378 ns0.881,444 B24 B1.00
ToString_Format_Flags_Large\main\corerun.exeAll?15.4803 ns1.00428 B24 B1.00
ToString_Format_Flags_Large\pr\corerun.exeAll?17.4866 ns1.131,444 B24 B1.00
ToString_Format_Flags_Large\main\corerun.exeAlld?17.6032 ns1.00428 B56 B1.00
ToString_Format_Flags_Large\pr\corerun.exeAlld?19.6840 ns1.111,444 B56 B1.00
ToString_Format_Flags_Large\main\corerun.exeAllf?17.1029 ns1.00428 B24 B1.00
ToString_Format_Flags_Large\pr\corerun.exeAllf?16.0028 ns0.931,444 B24 B1.00
ToString_Format_Flags_Large\main\corerun.exeAllg?16.6692 ns1.00428 B24 B1.00
ToString_Format_Flags_Large\pr\corerun.exeAllg?16.4078 ns0.981,444 B24 B1.00
ToString_Format_Flags_Large\main\corerun.exeAllx?24.2053 ns1.00428 B64 B1.00
ToString_Format_Flags_Large\pr\corerun.exeAllx?22.7144 ns0.941,415 B64 B1.00
ToString_NonFlags_Small\main\corerun.exeTopDirectoryOnly??11.9013 ns1.00313 B24 B1.00
ToString_NonFlags_Small\pr\corerun.exeTopDirectoryOnly??11.7533 ns0.98665 B24 B1.00
ToString_NonFlags_Small\main\corerun.exeAllDirectories??11.7413 ns1.00313 B24 B1.00
ToString_NonFlags_Small\pr\corerun.exeAllDirectories??11.4765 ns0.97665 B24 B1.00
ToString_NonFlags_Large\main\corerun.exeControl??11.6787 ns1.00313 B24 B1.00
ToString_NonFlags_Large\pr\corerun.exeControl??11.6246 ns1.00665 B24 B1.00
ToString_NonFlags_Large\main\corerun.exeFormat??12.5915 ns1.00313 B24 B1.00
ToString_NonFlags_Large\pr\corerun.exeFormat??11.6680 ns0.93665 B24 B1.00
ToString_Format_NonFlags\main\corerun.exeMondayg?13.4477 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exeMondayg?12.7797 ns0.941,444 B24 B1.00
ToString_Format_NonFlags\main\corerun.exeFridayX?23.2332 ns1.00428 B64 B1.00
ToString_Format_NonFlags\pr\corerun.exeFridayX?21.5598 ns0.931,415 B64 B1.00
ToString_NonFlags_Large\main\corerun.exeOtherNotAssigned??11.4982 ns1.00313 B24 B1.00
ToString_NonFlags_Large\pr\corerun.exeOtherNotAssigned??11.4304 ns0.99665 B24 B1.00
Parse_Flags\main\corerun.exe??Red65.6035 ns1.002,224 B24 B1.00
Parse_Flags\pr\corerun.exe??Red81.8236 ns1.251,155 B24 B1.00
TryParseGeneric_Flags\main\corerun.exe??Red28.3974 ns1.001,229 B-NA
TryParseGeneric_Flags\pr\corerun.exe??Red26.7545 ns0.94142 B-NA
StringFormat\main\corerun.exeRed??302.1524 ns1.00331 B232 B1.00
StringFormat\pr\corerun.exeRed??227.2187 ns0.75331 B96 B0.41
InterpolateIntoString\main\corerun.exeRed??166.0809 ns1.001,890 B232 B1.00
InterpolateIntoString\pr\corerun.exeRed??116.3095 ns0.702,068 B72 B0.31
InterpolateIntoSpan_Flags\main\corerun.exeRed??118.7156 ns1.00492 B160 B1.00
InterpolateIntoSpan_Flags\pr\corerun.exeRed??75.4937 ns0.64492 B-0.00
InterpolateIntoStringBuilder_Flags\main\corerun.exeRed??146.0972 ns1.00286 B160 B1.00
InterpolateIntoStringBuilder_Flags\pr\corerun.exeRed??100.1058 ns0.69286 B24 B0.15
ToString_Flags\main\corerun.exeYellow??16.1928 ns1.00313 B24 B1.00
ToString_Flags\pr\corerun.exeYellow??16.5742 ns1.03665 B24 B1.00
StringFormat\main\corerun.exeRed, Green??353.5309 ns1.00331 B416 B1.00
StringFormat\pr\corerun.exeRed, Green??267.8539 ns0.76331 B136 B0.33
InterpolateIntoString\main\corerun.exeRed, Green??211.1244 ns1.001,890 B416 B1.00
InterpolateIntoString\pr\corerun.exeRed, Green??148.7505 ns0.702,068 B112 B0.27
InterpolateIntoSpan_Flags\main\corerun.exeRed, Green??168.5889 ns1.00492 B304 B1.00
InterpolateIntoSpan_Flags\pr\corerun.exeRed, Green??105.7041 ns0.63492 B-0.00
InterpolateIntoStringBuilder_Flags\main\corerun.exeRed, Green??184.5203 ns1.00286 B304 B1.00
InterpolateIntoStringBuilder_Flags\pr\corerun.exeRed, Green??120.3897 ns0.66286 B24 B0.08
Parse_Flags\main\corerun.exe??Red, Orange, Yellow, Green, Blue129.7943 ns1.002,224 B24 B1.00
Parse_Flags\pr\corerun.exe??Red, Orange, Yellow, Green, Blue142.8291 ns1.101,155 B24 B1.00
TryParseGeneric_Flags\main\corerun.exe??Red, Orange, Yellow, Green, Blue86.5051 ns1.001,229 B-NA
TryParseGeneric_Flags\pr\corerun.exe??Red, Orange, Yellow, Green, Blue85.1191 ns0.99142 B-NA
ToString_Flags\main\corerun.exeRed, Orange, Yellow, Green, Blue??53.6118 ns1.00313 B112 B1.00
ToString_Flags\pr\corerun.exeRed, Orange, Yellow, Green, Blue??49.1623 ns0.92665 B112 B1.00
ToString_Format_NonFlags\main\corerun.exeSunday?11.8233 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exeSunday?13.5119 ns1.141,444 B24 B1.00
ToString_Format_NonFlags\main\corerun.exeTuesdayd?9.2690 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exeTuesdayd?10.0484 ns1.081,444 B24 B1.00
ToString_Format_NonFlags\main\corerun.exeThursdayf?18.3243 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exeThursdayf?17.4421 ns0.951,444 B24 B1.00
InterpolateIntoSpan_NonFlags\main\corerun.exeOrange, Yellow??167.9801 ns1.00492 B328 B1.00
InterpolateIntoSpan_NonFlags\pr\corerun.exeOrange, Yellow??101.7104 ns0.60492 B-0.00
InterpolateIntoStringBuilder_NonFlags\main\corerun.exeOrange, Yellow??185.5282 ns1.00286 B328 B1.00
InterpolateIntoStringBuilder_NonFlags\pr\corerun.exeOrange, Yellow??121.8469 ns0.66286 B24 B0.07
ToString_Format_NonFlags\main\corerun.exeSaturdayD?9.1248 ns1.00428 B24 B1.00
ToString_Format_NonFlags\pr\corerun.exeSaturdayD?10.2981 ns1.131,444 B24 B1.00
ToString_NonFlags_Large\main\corerun.exeUppercaseLetter??11.5365 ns1.00313 B24 B1.00
ToString_NonFlags_Large\pr\corerun.exeUppercaseLetter??11.5044 ns1.00665 B24 B1.00
ToString_Format_NonFlags\main\corerun.exeWednesdayx?22.9680 ns1.00428 B64 B1.00
ToString_Format_NonFlags\pr\corerun.exeWednesdayx?21.8983 ns0.951,415 B64 B1.00
ToString_Flags\main\corerun.exeYellow, Blue??35.6940 ns1.00313 B72 B1.00
ToString_Flags\pr\corerun.exeYellow, Blue??32.8618 ns0.92665 B72 B1.00

@ghost

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.

@ghost

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.

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

With this PR, it's ~3.5kb with tiered compilation off... with tiered compilation enables, it's actually more, at ~9.5kb.

I have pushed a change that reduces this to 6.5kb with tiered compilation enabled and 1058 bytes with tiered compilation disabled. It can be further reduced to 894 bytes by copy&pasting TryFormatUnconstrained implementation into TryFormat instead of depending on the JIT inliner to do it for you. I have not done it in my commit, but it may be worth doing. It should not actually increase the IL size thanks to IL body folding. Both methods should have identical IL and this copy&paste would be actually a tiny IL size reduction.

@stephentoub

Copy link
Copy Markdown
MemberAuthor

I have pushed a change that reduces this to 6.5kb with tiered compilation enabled and 1058 bytes with tiered compilation disabled.

Thanks. Would you recommend looking to replace Unsafe.As in other places like this as well (separate from this PR)? Or you view this as a temporary thing until inlining can be improved?

@jkotas

jkotas commented Nov 22, 2022

Copy link
Copy Markdown
Member

Would you recommend looking to replace Unsafe.As in other places like this as well (separate from this PR)?

Yes, I do not see any downsides. It is only possible when the source is stackallocated or in unmanaged memory.

Or you view this as a temporary thing until inlining can be improved?

It is not inlining problem. The problem is that the runtime has to materialize the generic Unsafe.As instantiations before the JIT can recognize them as intrinsics. It is hard to do something about it. If we were to do something about it, I think it would best to do that as IL-to-IL optimization pass at build time.

@DaZombieKiller

Copy link
Copy Markdown
Contributor

Would it be worthwhile to replace usages of Unsafe.SizeOf<T>() with sizeof(T) across the BCL as well, now that C# 11 allows that for all types?

@jkotas

Copy link
Copy Markdown
Member

Would it be worthwhile to replace usages of Unsafe.SizeOf() with sizeof(T) across the BCL as well, now that C# 11 allows that for all types?

Yes: #78741

Comment threadsrc/coreclr/System.Private.CoreLib/src/System/Enum.CoreCLR.cs Outdated
@radical

Copy link
Copy Markdown
Member

re:WasmBuildTests failures, these are some Blazor AOT builds getting oomkill'ed when linking. I have seen these before, and have an open issue for similar tests. I will investigate these separately, and this PR doesn't need to be blocked.

@stephentoub
stephentoub merged commit 62f3eb2 into dotnet:mainDec 6, 2022
@stephentoub
stephentoub deleted the enumtryformat branch December 6, 2022 03:37
@steveisok

Copy link
Copy Markdown
Member

The iOS & tvOS legs are failing to AOT System.Net.Http.Json. Need to investigate further to determine if it's related to this PR or not.

error : Precompiling failed for /tmp/helix/working/C0EE0A28/w/B12009A0/e/publish/System.Net.Http.Json.dll with exit code 139.

@stephentoub

Copy link
Copy Markdown
MemberAuthor

@steveisok, let me know if/how I can help your investigation.

@jkotas

Copy link
Copy Markdown
Member

The iOS & tvOS legs are failing to AOT System.Net.Http.Json.

I have opened #79279 on this.

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Need to investigate further to determine if it's related to this PR or not.

I tried reverting this in CI, and that revert PR still hits the failures:
https://github.com/dotnet/runtime/pull/79303/checks?check_run_id=9926753891

/tmp/helix/working/AF1E09C5/p/build/apple/AppleApp.targets(91,5): error : Precompiling failed for /tmp/helix/working/AF1E09C5/w/B29C097C/e/publish/System.Net.Http.Json.dll with exit code 139. [/private/tmp/helix/working/AF1E09C5/w/B29C097C/e/publish/ProxyProjectForAOTOnHelix.proj]
/tmp/helix/working/AF1E09C5/p/build/apple/AppleApp.targets(91,5): error : [/private/tmp/helix/working/AF1E09C5/w/B29C097C/e/publish/ProxyProjectForAOTOnHelix.proj]

so it's not this PR.

@JamesNK

JamesNK commented Dec 13, 2022

Copy link
Copy Markdown
Member

@stephentoub We noticed some enum order changes when updating ASP.NET Core to the latest runtime bits. The dependency update has been blocked for a long time, so it's hard to know precisely when the change happened. This PR might be the cause. See e4745f4 (#45475)

The enum values are fetched with reflection in Swashbuckle here - https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/1b1d3daedea177895062780cf09f1755647775c4/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs#L292

Was the order change intentional?

@stephentoub

Copy link
Copy Markdown
MemberAuthor

What are the enum's values and what is the order difference you observed?

@jkotas

Copy link
Copy Markdown
Member

Example:

foreach (var v in Enum.GetValues<MyEnum>())
Console.WriteLine(v);
enum MyEnum
{
A = -1,
B = 0,
C = 1
}

Before this change: B C A
After this change: A B C

Before this change, the values were always sorted as ulongs bit patterns. After this change, the values are sorted using their underlying type - signed, unsigned, floating point.

@stephentoub

Copy link
Copy Markdown
MemberAuthor

I found it:
https://github.com/dotnet/aspnetcore/blob/e4745f467ed378bec8ab19948085ee9e1ecd9350/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.Swagger.Tests/Proto/messages.proto#L16
Yes, that's intentional. Previously the values were being sorted as ulongs, which would cause negative values to come after everything else when the enum type was signed. Now they're sorted according to the actual type, so negative values come before the ones that come, you know, after :-)

@stephentoub

Copy link
Copy Markdown
MemberAuthor

Jan beat me by two minutes.

@ghostghost locked as resolved and limited conversation to collaborators Jan 12, 2023
@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Feb 17, 2023
@jeffhandleyjeffhandley added area-System.Runtime and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Feb 17, 2023
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

9 participants

@stephentoub@jkotas@DaZombieKiller@radical@steveisok@JamesNK@MichalPetryka@jeffhandley@heathbm