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

Add Generic Enum.TryFormat() #71590

Closed
wants to merge 6 commits into from

Conversation

heathbm
Copy link
Contributor

@heathbm heathbm commented Jul 3, 2022

Partial implementation of #57881

Benchmarks

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

Before implementation:

Method Mean Error StdDev Median Min Max Gen 0 Allocated
FormatG 33.96 ns 0.722 ns 0.676 ns 33.80 ns 33.07 ns 35.16 ns 0.0019 24 B
FormatD 27.03 ns 0.149 ns 0.132 ns 27.00 ns 26.84 ns 27.29 ns 0.0019 24 B
FormatX 35.46 ns 0.737 ns 0.789 ns 35.42 ns 33.99 ns 36.74 ns 0.0050 64 B
FormatF 33.69 ns 0.270 ns 0.225 ns 33.73 ns 33.17 ns 33.95 ns 0.0018 24 B

After implementation:

Method Mean Error StdDev Median Min Max Gen 0 Allocated
FormatG 32.983 ns 0.2221 ns 0.1969 ns 32.980 ns 32.588 ns 33.40 ns 0.0018 24 B
TryFormatG 14.332 ns 0.1794 ns 0.1678 ns 14.412 ns 13.943 ns 14.51 ns - -
FormatD 26.751 ns 0.8298 ns 0.9556 ns 26.348 ns 25.166 ns 28.47 ns 0.0019 24 B
TryFormatD 9.906 ns 0.1853 ns 0.1734 ns 9.862 ns 9.747 ns 10.35 ns - -
FormatX 34.408 ns 0.2204 ns 0.1954 ns 34.430 ns 33.866 ns 34.63 ns 0.0050 64 B
TryFormatX 17.993 ns 0.0465 ns 0.0435 ns 17.998 ns 17.913 ns 18.08 ns - -
FormatF 34.604 ns 0.2278 ns 0.2131 ns 34.486 ns 34.385 ns 34.99 ns 0.0018 24 B
TryFormatF 17.251 ns 0.0471 ns 0.0368 ns 17.247 ns 17.172 ns 17.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

@dotnet-issue-labeler
Copy link

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 ghost added the community-contribution Indicates that the PR has been added by a community member label Jul 3, 2022
@dotnet-issue-labeler
Copy link

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
Copy link

ghost commented Jul 3, 2022

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

using BenchmarkDotNet.Attributes;
using MicroBenchmarks;

namespace System.Tests
{
    [MemoryDiagnoser]
    [BenchmarkCategory(Categories.Libraries)]
    public class Perf_Enum
    {
        public enum Colors
        {
            Red = 0x1,
            Orange = 0x2,
            Yellow = 0x4,
            Green = 0x8,
            Blue = 0x10
        }

        [Benchmark]
        public void Format()
        {
            var destination = Enum.Format(typeof(Colors), Colors.Green, "F");
        }

        [Benchmark]
        public void TryFormat()
        {
            Span<char> destination = stackalloc char[5];
            Enum.TryFormat(Colors.Green, destination, out _, "F");
        }
    }
}
Method Mean Error StdDev Median Min Max Gen 0 Allocated
Format 31.90 ns 0.359 ns 0.318 ns 31.94 ns 31.04 ns 32.28 ns 0.0019 24 B
TryFormat 14.96 ns 0.053 ns 0.047 ns 14.95 ns 14.88 ns 15.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: -

Copy link
Contributor

@deeprobin deeprobin left a comment

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

src/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated Show resolved Hide resolved
src/libraries/System.Private.CoreLib/src/System/Enum.cs Outdated Show resolved Hide resolved
@jkotas
Copy link
Member

jkotas commented Jul 9, 2022

The Enum tests are failing.

@heathbm
Copy link
Contributor Author

heathbm commented Jul 13, 2022

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
Member

@jkotas was your feedback addressed?

@jkotas
Copy link
Member

jkotas commented Sep 6, 2022

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
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.

@dakersnar dakersnar 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
Contributor

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

@ghost ghost added the no-recent-activity label Oct 24, 2022
@ghost
Copy link

ghost commented Oct 24, 2022

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
Copy link

ghost commented Nov 8, 2022

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.

@ghost ghost closed this Nov 8, 2022
@dakersnar
Copy link
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.

@dakersnar dakersnar reopened this Nov 8, 2022
@dakersnar dakersnar added blocked Issue/PR is blocked on something - see comments and removed no-recent-activity labels Nov 8, 2022
@stephentoub
Copy link
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
Member

jkotas commented Nov 8, 2022

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
Copy link
Member

stephentoub commented Nov 8, 2022

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
Member

jkotas commented Nov 8, 2022

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
Member

I've assigned this to myself to land it.

@stephentoub stephentoub 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
- 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
Copy link
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)]
public string EnumToString(Colors value) => value.ToString();

[Benchmark]
[Arguments(SearchOption.TopDirectoryOnly)]
[Arguments(SearchOption.AllDirectories)]
[Arguments((SearchOption)(-1))]
public string EnumToString_SmallNonFlagsEnum(SearchOption value) => value.ToString();

[Benchmark]
[Arguments(UnicodeCategory.UppercaseLetter)]
[Arguments(UnicodeCategory.Control)]
[Arguments(UnicodeCategory.Format)]
[Arguments(UnicodeCategory.OtherNotAssigned)]
[Arguments((UnicodeCategory)42)]
public string EnumToString_LargeNonFlagsEnum(UnicodeCategory value) => 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")]
public string EnumToStringWithFormat(DayOfWeek value, string format) => value.ToString(format);

on my machine result in:

Method Job Toolchain value format Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Allocated Alloc Ratio
EnumToString_SmallNonFlagsEnum Job-EUILUD \main\corerun.exe -1 ? 30.90 ns 0.842 ns 0.935 ns 30.85 ns 29.520 ns 32.73 ns 1.00 0.00 0.0089 56 B 1.00
EnumToString_SmallNonFlagsEnum Job-HGADFP \pr\corerun.exe -1 ? 30.17 ns 0.354 ns 0.331 ns 30.10 ns 29.722 ns 30.83 ns 0.98 0.04 0.0089 56 B 1.00
EnumToString Job-EUILUD \main\corerun.exe 32 ? 30.64 ns 0.626 ns 0.586 ns 30.52 ns 29.962 ns 31.66 ns 1.00 0.00 0.0089 56 B 1.00
EnumToString Job-HGADFP \pr\corerun.exe 32 ? 29.51 ns 0.641 ns 0.569 ns 29.41 ns 28.576 ns 30.68 ns 0.96 0.02 0.0089 56 B 1.00
EnumToString Job-EUILUD \main\corerun.exe 36 ? 31.45 ns 0.756 ns 0.841 ns 31.40 ns 30.093 ns 32.96 ns 1.00 0.00 0.0088 56 B 1.00
EnumToString Job-HGADFP \pr\corerun.exe 36 ? 30.92 ns 0.697 ns 0.802 ns 30.94 ns 29.512 ns 32.16 ns 0.98 0.03 0.0089 56 B 1.00
EnumToString_LargeNonFlagsEnum Job-EUILUD \main\corerun.exe 42 ? 23.27 ns 0.948 ns 1.054 ns 23.21 ns 21.876 ns 25.69 ns 1.00 0.00 0.0089 56 B 1.00
EnumToString_LargeNonFlagsEnum Job-HGADFP \pr\corerun.exe 42 ? 22.20 ns 0.886 ns 1.020 ns 21.97 ns 20.722 ns 24.58 ns 0.95 0.06 0.0088 56 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Sunday 12.65 ns 0.597 ns 0.688 ns 12.45 ns 11.782 ns 14.28 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Sunday 13.14 ns 0.573 ns 0.636 ns 13.20 ns 12.169 ns 14.19 ns 1.05 0.08 0.0038 24 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Monday g 13.78 ns 0.327 ns 0.321 ns 13.80 ns 13.160 ns 14.46 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Monday g 14.35 ns 0.629 ns 0.725 ns 14.15 ns 13.465 ns 15.91 ns 1.05 0.07 0.0038 24 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Tuesday d 10.27 ns 0.324 ns 0.373 ns 10.25 ns 9.808 ns 11.09 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Tuesday d 10.67 ns 0.500 ns 0.535 ns 10.67 ns 10.003 ns 11.55 ns 1.04 0.06 0.0038 24 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Wednesday x 24.40 ns 0.604 ns 0.696 ns 24.28 ns 23.485 ns 25.72 ns 1.00 0.00 0.0101 64 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Wednesday x 23.63 ns 0.895 ns 0.994 ns 23.24 ns 22.779 ns 26.06 ns 0.97 0.05 0.0101 64 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Thursday f 19.31 ns 0.663 ns 0.737 ns 19.25 ns 18.538 ns 20.99 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Thursday f 18.21 ns 0.521 ns 0.600 ns 18.01 ns 17.386 ns 19.39 ns 0.94 0.04 0.0038 24 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Friday X 24.42 ns 0.529 ns 0.544 ns 24.30 ns 23.235 ns 25.43 ns 1.00 0.00 0.0101 64 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Friday X 24.39 ns 1.094 ns 1.260 ns 24.11 ns 22.857 ns 27.07 ns 1.01 0.06 0.0102 64 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe Saturday D 10.26 ns 0.349 ns 0.388 ns 10.17 ns 9.736 ns 10.99 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe Saturday D 10.43 ns 0.236 ns 0.197 ns 10.43 ns 10.102 ns 10.80 ns 1.03 0.04 0.0038 24 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe 7 G 16.72 ns 0.866 ns 0.998 ns 16.46 ns 15.390 ns 19.04 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe 7 G 16.91 ns 0.688 ns 0.792 ns 16.72 ns 15.934 ns 18.58 ns 1.02 0.09 0.0038 24 B 1.00
EnumToStringWithFormat Job-EUILUD \main\corerun.exe 8 F 27.46 ns 1.296 ns 1.492 ns 27.25 ns 25.967 ns 30.82 ns 1.00 0.00 0.0038 24 B 1.00
EnumToStringWithFormat Job-HGADFP \pr\corerun.exe 8 F 24.61 ns 0.537 ns 0.528 ns 24.78 ns 23.632 ns 25.43 ns 0.89 0.05 0.0038 24 B 1.00
EnumToString Job-EUILUD \main\corerun.exe Red, Orange, Yellow, Green, Blue ? 56.09 ns 2.247 ns 2.497 ns 55.84 ns 52.697 ns 62.04 ns 1.00 0.00 0.0176 112 B 1.00
EnumToString Job-HGADFP \pr\corerun.exe Red, Orange, Yellow, Green, Blue ? 50.89 ns 1.480 ns 1.704 ns 51.20 ns 48.498 ns 54.65 ns 0.91 0.05 0.0177 112 B 1.00
EnumToString_LargeNonFlagsEnum Job-EUILUD \main\corerun.exe UppercaseLetter ? 12.61 ns 0.408 ns 0.470 ns 12.54 ns 11.753 ns 13.31 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-HGADFP \pr\corerun.exe UppercaseLetter ? 12.07 ns 0.296 ns 0.304 ns 12.04 ns 11.664 ns 12.77 ns 0.96 0.05 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-EUILUD \main\corerun.exe Control ? 11.62 ns 0.211 ns 0.176 ns 11.60 ns 11.412 ns 12.05 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-HGADFP \pr\corerun.exe Control ? 11.09 ns 0.185 ns 0.164 ns 11.08 ns 10.898 ns 11.41 ns 0.95 0.02 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-EUILUD \main\corerun.exe Format ? 12.32 ns 0.266 ns 0.236 ns 12.25 ns 12.059 ns 12.80 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-HGADFP \pr\corerun.exe Format ? 12.32 ns 0.360 ns 0.400 ns 12.24 ns 11.825 ns 13.30 ns 1.00 0.04 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-EUILUD \main\corerun.exe OtherNotAssigned ? 12.44 ns 0.429 ns 0.494 ns 12.38 ns 11.765 ns 13.54 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString_LargeNonFlagsEnum Job-HGADFP \pr\corerun.exe OtherNotAssigned ? 12.19 ns 0.504 ns 0.580 ns 12.10 ns 11.369 ns 13.23 ns 0.98 0.06 0.0038 24 B 1.00
EnumToString_SmallNonFlagsEnum Job-EUILUD \main\corerun.exe TopDirectoryOnly ? 12.71 ns 0.590 ns 0.679 ns 12.50 ns 11.860 ns 13.87 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString_SmallNonFlagsEnum Job-HGADFP \pr\corerun.exe TopDirectoryOnly ? 11.58 ns 0.292 ns 0.312 ns 11.56 ns 11.258 ns 12.37 ns 0.92 0.05 0.0038 24 B 1.00
EnumToString_SmallNonFlagsEnum Job-EUILUD \main\corerun.exe AllDirectories ? 12.41 ns 0.389 ns 0.448 ns 12.23 ns 11.899 ns 13.42 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString_SmallNonFlagsEnum Job-HGADFP \pr\corerun.exe AllDirectories ? 11.72 ns 0.272 ns 0.212 ns 11.74 ns 11.248 ns 11.98 ns 0.95 0.03 0.0038 24 B 1.00
EnumToString Job-EUILUD \main\corerun.exe Yellow ? 16.38 ns 0.561 ns 0.623 ns 16.10 ns 15.764 ns 17.78 ns 1.00 0.00 0.0038 24 B 1.00
EnumToString Job-HGADFP \pr\corerun.exe Yellow ? 15.57 ns 0.349 ns 0.292 ns 15.61 ns 14.937 ns 16.00 ns 0.96 0.04 0.0038 24 B 1.00
EnumToString Job-EUILUD \main\corerun.exe Yellow, Blue ? 36.05 ns 1.333 ns 1.535 ns 35.79 ns 33.932 ns 39.00 ns 1.00 0.00 0.0113 72 B 1.00
EnumToString Job-HGADFP \pr\corerun.exe Yellow, Blue ? 35.32 ns 1.559 ns 1.795 ns 35.30 ns 32.570 ns 38.40 ns 0.98 0.04 0.0114 72 B 1.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)]
public bool InterpolateEnum(Colors value) => MemoryExtensions.TryWrite(s_scratch, $"{value} {value:g} {value:d} {value:x} {value:f}", out _);

results in:

Method Job Toolchain value Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Allocated Alloc Ratio
InterpolateEnum Job-GEPFJR \main\corerun.exe 32 158.75 ns 3.920 ns 4.357 ns 156.08 ns 155.11 ns 171.78 ns 1.00 0.00 0.0458 288 B 1.00
InterpolateEnum Job-ABMNOH \pr\corerun.exe 32 96.27 ns 1.065 ns 0.889 ns 96.10 ns 95.37 ns 97.89 ns 0.60 0.02 - - 0.00
InterpolateEnum Job-GEPFJR \main\corerun.exe Red, Green 173.81 ns 4.712 ns 5.238 ns 173.31 ns 164.44 ns 182.48 ns 1.00 0.00 0.0480 304 B 1.00
InterpolateEnum Job-ABMNOH \pr\corerun.exe Red, Green 113.90 ns 3.000 ns 3.334 ns 113.61 ns 108.63 ns 120.92 ns 0.66 0.03 - - 0.00

Other generic Enum methods also improve, e.g.

private Colors _colorValue = Colors.Blue;
private DayOfWeek _dayOfWeekValue = DayOfWeek.Saturday;

[Benchmark]
public bool IsDefined() => Enum.IsDefined(_colorValue);

[Benchmark]
public bool IsDefined_NonFlags() => Enum.IsDefined(_dayOfWeekValue);

[Benchmark]
public string GetName() => Enum.GetName(_colorValue);

[Benchmark]
public string GetName_NonFlags() => Enum.GetName(_dayOfWeekValue);

[Benchmark]
public string[] GetNames() => Enum.GetNames<Colors>();

results in:

Method Job Toolchain Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Allocated Alloc Ratio
IsDefined Job-UKYMDD \main\corerun.exe 6.572 ns 0.1094 ns 0.0854 ns 6.559 ns 6.471 ns 6.715 ns 1.00 0.00 - - NA
IsDefined Job-RHTFZD \pr\corerun.exe 3.830 ns 0.0989 ns 0.1100 ns 3.773 ns 3.728 ns 4.091 ns 0.58 0.02 - - NA
IsDefined_NonFlags Job-UKYMDD \main\corerun.exe 3.751 ns 0.0670 ns 0.0627 ns 3.762 ns 3.652 ns 3.855 ns 1.00 0.00 - - NA
IsDefined_NonFlags Job-RHTFZD \pr\corerun.exe 1.407 ns 0.0512 ns 0.0479 ns 1.396 ns 1.357 ns 1.506 ns 0.38 0.01 - - NA
GetName Job-UKYMDD \main\corerun.exe 9.214 ns 0.5445 ns 0.6271 ns 9.119 ns 8.359 ns 10.771 ns 1.00 0.00 - - NA
GetName Job-RHTFZD \pr\corerun.exe 5.003 ns 0.1648 ns 0.1832 ns 5.017 ns 4.672 ns 5.262 ns 0.54 0.04 - - NA
GetName_NonFlags Job-UKYMDD \main\corerun.exe 4.945 ns 0.2827 ns 0.3255 ns 4.914 ns 4.603 ns 5.786 ns 1.00 0.00 - - NA
GetName_NonFlags Job-RHTFZD \pr\corerun.exe 2.499 ns 0.1435 ns 0.1653 ns 2.509 ns 2.311 ns 2.816 ns 0.51 0.05 - - NA
GetNames Job-UKYMDD \main\corerun.exe 15.713 ns 0.7786 ns 0.8966 ns 15.475 ns 14.753 ns 17.493 ns 1.00 0.00 0.0101 64 B 1.00
GetNames Job-RHTFZD \pr\corerun.exe 14.534 ns 0.3457 ns 0.3065 ns 14.432 ns 14.123 ns 15.052 ns 0.93 0.07 0.0101 64 B 1.00

@@ -24,7 +24,7 @@
namespace System
{
[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
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?

Copy link
Member

@stephentoub stephentoub Nov 15, 2022

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.

Copy link
Contributor

@drieseng drieseng Nov 16, 2022

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
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 (int i = 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
Member

jkotas commented Nov 16, 2022

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)]
static bool TryFormat<TEnum>(TEnum value, Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan<char> format = default) where TEnum : struct, Enum
{
    EnumInfo enumInfo = GetEnumInfo(typeof(TEnum));
    
    // We may want to tweak the JIT so that it dead-code eliminates unreachable branches even in tier 0
    Type underlyingType = typeof(TEnum).GetEnumUnderlyingType();
    if (underlyingType == typeof(int)) return TryFormatPrimitive<int>(Unsafe.As<TEnum, int>(ref value), destination, charsWritten, format, enumInfo);
    if (underlyingType == typeof(long)) return TryFormatPrimitive<long>(Unsafe.As<TEnum, long>(ref value), destination, charsWritten, format, enumInfo);
    ...
}

// This method is only instantiated per finite set of primitive types.
static bool TryFormatPrimitive<TUnderlyingType>(TUnderlyingType value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, EnumInfo enumInfo) where TUnderlyingType: ISpanFormattable
{
...
}

@stephentoub
Copy link
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
Member

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

@EgorBo

@EgorBo
Copy link
Member

EgorBo commented Nov 16, 2022

Reference in new

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

@ghost ghost locked as resolved and limited conversation to collaborators Dec 17, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Runtime community-contribution Indicates that the PR has been added by a community member new-api-needs-documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.