はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】FastEnumというライブラリを用いて列挙型関連のメソッドを爆速+ゼロアロケーションで実行させる(Static Type Cachingによる高速化)

はじめに

今回は列挙型関連のメソッドを爆速+ゼロアロケーションで実行できるFastEnumというライブラリを紹介したいと思います。

.NETメソッドとの比較結果

概要

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

Rider上でNuGetパッケージからインストールした様子

使い方

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

違いはEnumMemberAttributeAllowMultiple = falseですが、LabelAttributeAllowMultiple = 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を利用しているからです。

www.hanachiru-blog.com

FastEnumのコアはFastEnum_Cache.csですね。
github.com

ここでスレッドセーフが保証されながら必ず一度しか実行されず値がキャッシュされます。ただ欠点としては一度キャッシュされるとずっとメモリを占有し続けてしつづけてしまうことには注意してください。