はじめに
今回は列挙型関連のメソッドを爆速+ゼロアロケーションで実行できるFastEnum
というライブラリを紹介したいと思います。
概要
FastEnum
を用いることでゼロアロケーションで高速に列挙型関連のメソッドを動作させることができます。
FastEnum is the fastest enum utilities for C#/.NET. It's much faster than .NET Core, and also faster than Enums.NET that is similar library. Provided methods are all achieved zero allocation and are designed easy to use like System.Enum. This library is quite useful to significantly improve your performance because enum is really popular feature.
// DeepL翻訳
FastEnum は、C#/.NET 用の最速の列挙型ユーティリティです。.NET Coreよりもはるかに高速で、同様のライブラリであるEnums.NETよりも高速です。提供されるメソッドはすべてゼロ割り当てを実現し、System.Enumのように使いやすく設計されています。enumは本当によく使われる機能なので、このライブラリはパフォーマンスを大幅に向上させるのに非常に便利です。
GitHub - xin9le/FastEnum: The world fastest enum utilities for C#/.NET
作者さんが書かれたブログにも詳細が書いてあるので、気になる方はチェックしてみてください。
xin9le.hatenablog.jp
インストール方法
NuGetから
www.nuget.org
使い方
// GetValues // .NET: Fruits[]? _ = Enum.GetValues(typeof(Fruits)) as Fruits[]; // 値: Apple, Pear, Quince, Peach, Cherry, Orange, Mandarin, Lemon, Lime, Strawberry, Grape, Blueberry, Raspberry, Melon, Tomato, Banana, Mango IReadOnlyList<Fruits> values = FastEnum.GetValues<Fruits>(); // GetNames // .NET: string[] _ = Enum.GetNames(typeof(Fruits)); // 値 : Apple, Pear, Quince, Peach, Cherry, Orange, Mandarin, Lemon, Lime, Strawberry, Grape, Blueberry, Raspberry, Melon, Tomato, Banana, Mango IReadOnlyList<string> names = FastEnum.GetNames<Fruits>(); // GetName // .NET: string? _ = Enum.GetName(typeof(Fruits), Fruits.Grape); // 値: Grape string? name = Fruits.Grape.ToName(); // ToString // .NET: string _ = Fruits.Grape.ToString(); // 値: Grape string text = Fruits.Grape.FastToString(); // IsDefines // .NET: bool _ = Enum.IsDefined(typeof(Fruits), 10); // True bool isDefined = FastEnum.IsDefined<Fruits>(10); // Parse // .NET: Fruits _ = Enum.Parse<Fruits>(Grape); // 値: Grape Fruits fruits = FastEnum.Parse<Fruits>(Grape); // TryParse // .NET: Enum.TryParse<Fruits>(Grape, out var value); // 値: Grape FastEnum.TryParse<Fruits>(Grape, out var value);
// 列挙型の定義 public enum Fruits { Apple, Pear, Quince, Peach, Cherry, Orange, Mandarin, Lemon, Lime, Strawberry, Grape, Blueberry, Raspberry, Melon, Tomato, Banana, Mango, }
列挙型のName・Value・FieldInfoなどを一度に取得したい場合
Meber
というクラスを利用します。
github.com
Member<Fruits>? member = Fruits.Grape.ToMember(); // string : Grape Console.WriteLine(member.Name); // Fruits : Grape Console.WriteLine(member.Value); // bool : True Console.WriteLine(member.FieldInfo.IsPublic); // Enumに定義されているすべてに対してMemberを取得 foreach (Member<Fruits> x in FastEnum.GetMembers<Fruits>()) { }
列挙型に情報を付与する
.NETに用意されているEnumMemberAttribute
か、FastEnum
が提供しているLabelAttribute
で情報を付与・取得できます。
learn.microsoft.com
違いはEnumMemberAttribute
がAllowMultiple = false
ですが、LabelAttribute
はAllowMultiple = true
です。
public enum Employee { // System.Runtime.Serialization.EnumMemberAttribute [EnumMember(Value = "SATO")] Sato, Suzuki, // FastEnumUtility.LabelAttribute [Label("TAKAHASHI")] [Label("takahashi", 1)] Takahashi } public static void Main(string[] args) { // EnumMemberAttributeの取得 string? x = Employee.Sato.GetEnumMemberValue(); // SATO Console.WriteLine(x); // LabelAttributeの取得 string? y = Employee.Takahashi.GetLabel(); string? z = Employee.Takahashi.GetLabel(1); // TAKAHASHI Console.WriteLine(y); // takahashi Console.WriteLine(z); }
制約
System.Enum
にはあったtypeof(TEnum)
を用いたオーバーロードがないことや、カンマ区切りの文字列をParse
できない制約があります。
詳細は筆者のブログをみてみてください。
実験
public class Program { public static void Main(string[] args) { BenchmarkRunner.Run<EnumTest>(); } } public enum Fruits { Apple, Pear, Quince, Peach, Cherry, Orange, Mandarin, Lemon, Lime, Strawberry, Grape, Blueberry, Raspberry, Melon, Tomato, Banana, Mango, } [MemoryDiagnoser] public class EnumTest { private const string Grape = "Grape"; [Benchmark] public void GetValues_DotNet8Enum() { _ = Enum.GetValues(typeof(Fruits)) as Fruits[]; } [Benchmark] public void GetValues_FastEnum() { _ = FastEnum.GetValues<Fruits>(); } [Benchmark] public void GetNames_DotNet8Enum() { _ = Enum.GetNames(typeof(Fruits)); } [Benchmark] public void GetNames_FastEnum() { _ = FastEnum.GetNames<Fruits>(); } [Benchmark] public void GetName_DotNet8Enum() { _ = Enum.GetName(typeof(Fruits), Fruits.Grape); } [Benchmark] public void GetName_FastEnum() { _ = Fruits.Grape.ToName(); } [Benchmark] public void ToString_DotNet8Enum() { _ = Fruits.Grape.ToString(); } [Benchmark] public void ToString_FastEnum() { _ = Fruits.Grape.FastToString(); } [Benchmark] public void IsDefines_DotNet8Enum() { _ = Enum.IsDefined(typeof(Fruits), 10); } [Benchmark] public void IsDefines_FastEnum() { _ = FastEnum.IsDefined<Fruits>(10); } [Benchmark] public void Parse_DotNet8Enum() { _ = Enum.Parse<Fruits>(Grape); } [Benchmark] public void Parse_FastEnum() { _ = FastEnum.Parse<Fruits>(Grape); } [Benchmark] public void TryParse_DotNet8Enum() { Enum.TryParse<Fruits>(Grape, out var value); } [Benchmark] public void TryParse_FastEnum() { FastEnum.TryParse<Fruits>(Grape, out var value); } }
Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|
GetValues_DotNet8Enum | 66.2492 ns | 1.2317 ns | 1.0285 ns | 66.4235 ns | 0.0114 | - | 96 B |
GetValues_FastEnum | 0.0148 ns | 0.0095 ns | 0.0084 ns | 0.0128 ns | - | - | - |
GetNames_DotNet8Enum | 17.6867 ns | 0.0859 ns | 0.0717 ns | 17.6979 ns | 0.0191 | 0.0000 | 160 B |
GetNames_FastEnum | 0.0004 ns | 0.0009 ns | 0.0008 ns | 0.0000 ns | - | - | - |
GetName_DotNet8Enum | 16.0112 ns | 0.0657 ns | 0.0583 ns | 16.0075 ns | 0.0029 | - | 24 B |
GetName_FastEnum | 0.1692 ns | 0.0020 ns | 0.0018 ns | 0.1685 ns | - | - | - |
ToString_DotNet8Enum | 6.1711 ns | 0.0178 ns | 0.0149 ns | 6.1656 ns | 0.0029 | - | 24 B |
ToString_FastEnum | 0.3293 ns | 0.0026 ns | 0.0023 ns | 0.3288 ns | - | - | - |
IsDefines_DotNet8Enum | 8.3594 ns | 0.0364 ns | 0.0304 ns | 8.3488 ns | 0.0029 | - | 24 B |
IsDefines_FastEnum | 1.0607 ns | 0.0039 ns | 0.0032 ns | 1.0617 ns | - | - | - |
Parse_DotNet8Enum | 28.4031 ns | 0.1661 ns | 0.1387 ns | 28.3593 ns | - | - | - |
Parse_FastEnum | 6.1529 ns | 0.0177 ns | 0.0138 ns | 6.1490 ns | - | - | - |
TryParse_DotNet8Enum | 28.4696 ns | 0.0777 ns | 0.0689 ns | 28.4793 ns | - | - | - |
TryParse_FastEnum | 6.3234 ns | 0.1440 ns | 0.2156 ns | 6.2321 ns | - | - | - |
BenchmarkDotNet v0.13.12, macOS Sonoma 14.4.1 Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores .NET SDK 8.0.203 [Host] : .NET 8.0.3 (8.0.324.11423), Arm64 RyuJIT AdvSIMD DefaultJob : .NET 8.0.3 (8.0.324.11423), Arm64 RyuJIT AdvSIMD
全体的に速度がかなり向上している + ゼロアロケーションを達成しているようですね。
早い理由
前このブログでも紹介したStatic Type Caching
を利用しているからです。
FastEnum
のコアはFastEnum_Cache.cs
ですね。
github.com
ここでスレッドセーフが保証されながら必ず一度しか実行されず値がキャッシュされます。ただ欠点としては一度キャッシュされるとずっとメモリを占有し続けてしつづけてしまうことには注意してください。