Location>code7788 >text

Explore Enum Optimization

Popularity:190 ℃/2024-08-31 13:46:30

Explore Enum Optimization

The main point is to explore how to make enum more efficient

The optimization techniques involved are not entirely self-invented

Much of the content is referenced in the following items

  • FastEnum
  • runtime

Main optimization tools

It's really all about space for time, lots of caching.

Wrapping entry methods and source-generators generates

However, this project tries to encapsulate the entry method,ModuleInitializersource-generators to avoid impacts on usage (actually, it's more about trying to figure out how to avoid using theinterceptors

    public static class Enums<T> where T : struct, Enum
    {
        public static bool IsFlags => CheckInfo().IsFlags;
        public static bool IsEmpty => CheckInfo().IsEmpty;

        internal static IEnumInfo<T> Info;

        [MethodImpl()]
        internal static IEnumInfo<T> CheckInfo()
        {
            if (Info == null)
            {
                Info = new EnumInfo<T>();
            }
            return Info;
        }

        public static T Parse(string name, bool ignoreCase)
        {
            if (CheckInfo().TryParse(name, ignoreCase, out var result))
                return result;
            throw new ArgumentException($"Specified value '{name}' is not defined.", nameof(name));
        }

Doing so will primarily allow you to utilize thesource-generators Generate enum processing code.

adapted and approved byModuleInitializer Enable the generated enum code at runtime

The following is an example of generating enum code

internal class EnumInfoAD125120120540FC9AA056E2DD394A7C : EnumBase<global::Benchmark.Fruits2>
    {
        public override bool IsDefined(string name)
        {
            return name switch
            {
                nameof(global::Benchmark.) => true,
nameof(global::Benchmark.) => true,
nameof(global::Benchmark.) => true,
nameof(global::Benchmark.) => true,
                _ => false,
            };
        }

        public override string? GetName(global::Benchmark.Fruits2 t)
        {
            switch (t)
            {
                case global::Benchmark.: return nameof(global::Benchmark.);
case global::Benchmark.: return nameof(global::Benchmark.);
case global::Benchmark.: return nameof(global::Benchmark.);
case global::Benchmark.: return nameof(global::Benchmark.);
                default:
                    return null;
            }
        }

        protected override bool TryParseCase(in ReadOnlySpan<char> name, out global::Benchmark.Fruits2 result)
        {
            switch (name)
            {
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                default:
                    result = default;
                    return false;
            }
        }

        protected override bool TryParseIgnoreCase(in ReadOnlySpan<char> name, out global::Benchmark.Fruits2 result)
        {
            switch (name)
            {
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                case ReadOnlySpan<char> current when (nameof(global::Benchmark.).AsSpan(), global::):
                    result = global::Benchmark.;
                    return true;
                default:
                    result = default;
                    return false;
            }
        }
    }

        internal static partial class EnumsF1029F0E5915401BBDD8559E2B5289B1
    {
        [ModuleInitializer]
        internal static void Init4771B8A4BD2E4761973279D81E61089C()
        {
            global::<global::Benchmark.Fruits2>(new EnumInfoAD125120120540FC9AA056E2DD394A7C());
        }
    }

However, there is some performance loss in encapsulating the entry in this way

Space for time

Of course if you don't use thesource-generatorsThe corresponding function also has a default implementation

Some of the code looks like this, but most of the stuff is cached in memory.

    public class EnumInfo<T> : IEnumInfo<T> where T : struct, Enum
    {
        private readonly string[] names;
        private readonly T[] values;
        private readonly (string Name, T Value)[] members;
        private readonly FastReadOnlyDictionary<string, T> membersByName;
        private readonly FastReadOnlyDictionary<T, (string Name, EnumMemberAttribute Member, FastReadOnlyDictionary<int, string> Labels)> namesByMember;
        private readonly Type underlyingType;
        private readonly TypeCode underlyingTypeCode;

        public bool IsFlags { get; private set; }
        public bool IsEmpty =>  == 0;

        public EnumInfo() : base()
        {
            var t = typeof(T);
            names = (t);
            members = (i => (i, (T)(t, i))).ToArray();
            values = (i => ).ToArray();
            membersByName = (i => , i => );
            namesByMember = ().DistinctBy(i => ).ToFastReadOnlyDictionary(i => , i =>
            {
                var fieldInfo = ()!;
                return (, <EnumMemberAttribute>(), <LabelAttribute>().DistinctBy(i => ).ToFastReadOnlyDictionary(x => , x => ));
            });
            underlyingType = (t);
            underlyingTypeCode = (underlyingType);
            IsFlags = (typeof(FlagsAttribute), true);
        }

        [MethodImpl()]
        public bool TryParseIgnoreCase(in ReadOnlySpan<char> name, out T result)
        {
            foreach (var member in ())
            {
                if (((), ))
                {
                    result = ;
                    return true;
                }
            }
            result = default;
            return false;
        }

enum Conversion method

There are also some enum conversion methods that don't need to be unpacked and boxed, which remove the type-checking logic, so the theory is that they can only be used normally without problems.

public static T ToEnum(int value)

public static T ToEnum(byte value)

public static T ToEnum(Int16 value)

public static T ToEnum(Int64 value)

...

performance testing

A quick performance test, part of the code is as follows

public enum Fruits
    {
        Apple,
        Lemon,
        Melon,
        Banana,
        Lemon1,
        Melon2,
        Banana3,
        Lemon11,
        Melon21,
        Banana31,
        Lemon12,
        Melon22,
        Banana32,
        Lemon13,
        Melon23,
        Banana33,
        Lemon131,
        Melon231,
        Banana331,
        Lemon14,
        Melon24,
        Banana34,
    }

    [MemoryDiagnoser, Orderer(summaryOrderPolicy: ), GroupBenchmarksBy(), CategoriesColumn]
    public class EnumBenchmarks
    {
        private readonly EnumInfo<Fruits> test;

        public EnumBenchmarks()
        {
            test = new EnumInfo<Fruits>();
        }

        [Benchmark(Baseline = true), BenchmarkCategory("IgnoreCase")]
        public Fruits ParseIgnoreCase()
        {
            return <Fruits>("melon", true);
        }

        [Benchmark, BenchmarkCategory("IgnoreCase")]
        public Fruits FastEnumParseIgnoreCase()
        {
            return <Fruits>("melon", true);
        }

        [Benchmark, BenchmarkCategory("IgnoreCase")]
        public Fruits SVEnumsParseIgnoreCase()
        {
            Enums<Fruits>.TryParse("melon", true, out var v);
            return v;
        }

        [Benchmark, BenchmarkCategory("IgnoreCase")]
        public Fruits EnumInfoParseIgnoreCase()
        {
            ("melon", true, out var v);
            return v;
        }

        [Benchmark(Baseline = true)]
        public Fruits Parse()
        {
            return <Fruits>("Melon", false);
        }

        [Benchmark]
        public Fruits FastEnumParse()
        {
            return <Fruits>("Melon", false);
        }

        [Benchmark]
        public Fruits SVEnumsParse()
        {
            Enums<Fruits>.TryParse("Melon", out var v);
            return v;
        }

        [Benchmark]
        public Fruits EnumInfoParse()
        {
            ("Melon", false, out var v);
            return v;
        }

        ...

    [MemoryDiagnoser, Orderer(summaryOrderPolicy: ), GroupBenchmarksBy(), CategoriesColumn]
    public class ToEnumBenchmarks
    {
        private readonly EnumInfo<Fruits> test;

        public ToEnumBenchmarks()
        {
            test = new EnumInfo<Fruits>();
            //<Fruits>(new TestIEnumInfo());
        }

        [Benchmark(Baseline = true), BenchmarkCategory("ToEnumInt")]
        public Fruits ToEnumInt()
        {
            return (Fruits)(typeof(Fruits), 11);
        }

        [Benchmark, BenchmarkCategory("ToEnumInt")]
        public Fruits SVEnumsToEnumInt()
        {
            return Enums<Fruits>.ToEnum(11);
        }

        [Benchmark, BenchmarkCategory("ToEnumInt")]
        public Fruits ToEnumIntByte()
        {
            return (Fruits)(typeof(Fruits), (byte)11);
        }

        [Benchmark, BenchmarkCategory("ToEnumInt")]
        public Fruits SVEnumsToEnumIntByte()
        {
            return Enums<Fruits>.ToEnum((byte)11);
        }

        [Benchmark, BenchmarkCategory("ToEnumInt")]
        public Fruits ToEnumIntObject()
        {
            return (Fruits)(typeof(Fruits), (object)11);
        }

        [Benchmark, BenchmarkCategory("ToEnumInt")]
        public Fruits SVEnumsToEnumIntObject()
        {
            return Enums<Fruits>.ToEnum((object)11);
        }
    }

The results are as follows


BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.100-preview.5.24307.3
  [Host]     : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2


Method Mean Error StdDev Median Ratio RatioSD Gen0 Allocated Alloc Ratio
SVEnumsToEnumInt 0.9796 ns 0.0062 ns 0.0055 ns 0.9781 ns 0.02 0.00 - - 0.00
SVEnumsToEnumIntByte 1.0990 ns 0.0089 ns 0.0074 ns 1.0966 ns 0.03 0.00 - - 0.00
SVEnumsToEnumIntObject 5.1211 ns 0.0842 ns 0.0746 ns 5.1295 ns 0.12 0.00 0.0029 24 B 1.00
ToEnumIntByte 40.9720 ns 0.2100 ns 0.1861 ns 40.9065 ns 1.00 0.03 0.0029 24 B 1.00
ToEnumInt 41.1962 ns 0.8452 ns 1.4122 ns 40.4985 ns 1.00 0.05 0.0029 24 B 1.00
ToEnumIntObject 48.2590 ns 0.4380 ns 0.3882 ns 48.0802 ns 1.17 0.04 0.0057 48 B 2.00
Method Categories Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
SVEnumsParse 2.8382 ns 0.0508 ns 0.0450 ns 0.12 0.00 - - - NA
FastEnumParse 6.9671 ns 0.0437 ns 0.0388 ns 0.28 0.00 - - - NA
EnumInfoParse 7.1513 ns 0.1049 ns 0.0930 ns 0.29 0.00 - - - NA
Parse 24.5338 ns 0.0548 ns 0.0485 ns 1.00 0.00 - - - NA
FastEnumGetName GetName 0.8608 ns 0.0175 ns 0.0155 ns 0.32 0.01 - - - NA
EnumInfoGetName GetName 1.4291 ns 0.0147 ns 0.0130 ns 0.54 0.01 - - - NA
SVEnumsGetName GetName 1.6210 ns 0.0148 ns 0.0131 ns 0.61 0.01 - - - NA
GetName GetName 2.6512 ns 0.0150 ns 0.0125 ns 1.00 0.01 - - - NA
SVEnumsGetNames GetNames 0.2539 ns 0.0061 ns 0.0051 ns 0.01 0.00 - - - 0.00
FastEnumGetNames GetNames 0.6874 ns 0.0195 ns 0.0163 ns 0.03 0.00 - - - 0.00
GetNames GetNames 21.0463 ns 0.4645 ns 0.5162 ns 1.00 0.03 0.0239 0.0001 200 B 1.00
SVEnumsGetValues GetValues 0.3022 ns 0.0296 ns 0.0277 ns 0.009 0.00 - - - 0.00
FastEnumGetValues GetValues 0.6683 ns 0.0098 ns 0.0082 ns 0.021 0.00 - - - 0.00
GetValues GetValues 32.5145 ns 0.6732 ns 0.5968 ns 1.000 0.03 0.0134 - 112 B 1.00
SVEnumsParseIgnoreCase IgnoreCase 3.0465 ns 0.0680 ns 0.0727 ns 0.12 0.00 - - - NA
EnumInfoParseIgnoreCase IgnoreCase 10.1299 ns 0.1660 ns 0.1472 ns 0.42 0.01 - - - NA
FastEnumParseIgnoreCase IgnoreCase 10.3531 ns 0.0807 ns 0.0674 ns 0.42 0.00 - - - NA
ParseIgnoreCase IgnoreCase 24.3767 ns 0.1270 ns 0.1060 ns 1.00 0.01 - - - NA
SVEnumsIsDefinedName IsDefinedName 2.7188 ns 0.0111 ns 0.0098 ns 0.11 0.00 - - - NA
EnumInfoIsDefinedName IsDefinedName 6.6075 ns 0.0190 ns 0.0148 ns 0.26 0.00 - - - NA
FastEnumIsDefinedName IsDefinedName 6.7011 ns 0.0388 ns 0.0303 ns 0.26 0.00 - - - NA
IsDefinedName IsDefinedName 25.3131 ns 0.2064 ns 0.1829 ns 1.00 0.01 - - - NA

For the full code see/fs7744/Enums