列挙体と文字列を紐づける実装のパフォーマンス調査

## 更新
- 2024年2月4日　投稿
- 2025年4月1日　指摘を受け、正確なパフォーマンス検証記事となるよう全面修正

# 0. この記事は？
もう、タイトル通り。
列挙体は、デフォでは（数字はできても）文字列にはキャストできないので、ではどんな実装ならばパフォーマンスがいいのか、調べて考察するというもの。

:::note
#### 環境
- Unity
    - Unity6000.0.40f1
    - .NET Standard2.1
    - IL2CPP
- BenchmarkDotNetでの計測に使用したC#プロジェクト
    - .NET9
    - C#9.0
:::

# 1. 思いつく実装
とりあえず、パフォーマンス比較対象になりうる実装を挙げていきます。
なお、以下のような列挙体をキャストすると思ってください。

```cs:Fruits.cs
using System.Runtime.Serialization;

public enum Fruits
{
    [EnumMember(Value = "Apple")]
    Apple,
    [EnumMember(Value = "Banana")]
    Banana,
    [EnumMember(Value = "Peach")]
    Peach
}
```
## 1-1. 列挙体をそのまま `ToString()` しちゃう
```csharp:ToStringCase.cs
string hoge = Fruits.Apple.ToString();
```
一番定番で、一番手軽だと思います。
こちら、機能的なデメリットとして、例えば `"Main Window"` みたいな半角スペースを入れた文字列などを返してほしいケースに対応できません。
パフォーマンス面も、毎回 `ToString()` を呼び出すたびに文字列を生成するのは、ちょっとよろしくありません。
この記事は速度検証がメインですが、メモリを話をするなら、これが一番よくないかも。

## 1-2. 連想配列を使うパターン
```csharp:DictionaryCase1.cs
static readonly Dictionary<Fruits, string> dic = new Dictionary<Fruits, string>
{
    { Fruits.Apple, "Apple" },
    { Fruits.Banana, "Banana" },
    { Fruits.Peach, "Peach" }
};

string hoge = dic[Fruits.Apple];
```
`ToString()` を使わずに、となると、誰もが一瞬は `Dictionary<T>` が脳裏をよぎるはず。
機能面は、先述の事例でもあった半角スペース入り文字列などもValueに登録できますので良好。
一回キャッシュしている形ではあるので、`ToString()` ほど新しいインスタンスがポコポコ起こることもないし。
悪くない選択肢といえます。

蛇足ですが、今回Keyにしている列挙体は `IEquatable<T>` が実装されていません。
`Dictionary<T>` のKeyにする型は `IEquatable<T>` を実装するべき、というC#ハックがあるんですが、列挙体に関して言えば、確か.NET Framework 4.6くらいで速度改善されていたはず。
なので、上記実装でもそんなに遅くなかったはずですが、念のため対策したものも検証しておきましょう。

```cs:DictionaryCase2.cs
static readonly Dictionary<Fruits, string> dic2 = new Dictionary<Fruits, string>(new FruitsEqualityComparer())
{
    { Fruits.Apple, "Apple" },
    { Fruits.Banana, "Banana" },
    { Fruits.Peach, "Peach" }
};

class FruitsEqualityComparer : IEqualityComparer<Fruits>
{
    public bool Equals(Fruits x, Fruits y)
    {
        return (int)x == (int)y;
    }

    public int GetHashCode(Fruits obj)
    {
        return ((int)obj).GetHashCode();
    }
}
```
外部から `IEquatable<T>` 周りの挙動を制御できる `IEqualityComparer<T>` を実装したクラスを `Dictionary<T>` に渡してみました。

また、`IEqualityComparer<T>` なんて大がかり（？）な実装しなくても、`IEquatable<T>` を実装している型にキャストすればいいのでは、という案もあると思います。

```cs:DictionaryCase3.cs
static readonly Dictionary<int, string> dic3 = new Dictionary<int, string>
{
    { (int)Fruits.Apple, "Apple" },
    { (int)Fruits.Banana, "Banana" },
    { (int)Fruits.Peach, "Peach" }
};
    
string hoge = dic3[(int)Fruits.Apple];
```

書き心地あんまりよくないですが、これも検証しておきましょう。

## 1-3. リストや配列を使う
`Dictionary<T>` は内部でハッシュテーブルアルゴリズムを利用した挙動をするわけですが、シンプルな配列やリストの方が速いのでは？という案もあるかと思います。
これも検証しておきましょう。

```cs:ListCase.cs
static readonly List<string> list = new List<string>
{
    "Apple",
    "Banana",
    "Peach"
};
    
string hoge = list[(int)Fruits.Apple];
```

```cs:ArrayCase.cs
static readonly string[] array = new string[3]
{
    "Apple",
    "Banana",
    "Peach"
};
    
string hoge = array[(int)Fruits.Apple];
```

ここらへんの実装は、正直書き心地や可読性考えると、到底利用できる選択肢ではない気がします。
ただ理論値ここらへんが最速なのは間違いない。

## 1-4. [FastEnum](https://github.com/xin9le/FastEnum)を使う

https://github.com/xin9le/FastEnum

ちょっと派生して、ライブラリを使った実装の計測もしてみたいと思います。
C#の列挙体周りのライブラリで名高い「FastEnum」を使い、事前に `System.Runtime.Serialization.EnumMemberAttribute` を使った列挙体定義をしていると、かなり書き心地よく、以下のような実装が書けます。

```cs:FastEnumCase.cs
string hoge = Fruits.Apple.GetEnumMemberValue();
```

この記事は、Unity環境で利用するべき実装を検討する主旨があるのですが、FastEnumを利用するにあたり注意しなければいけないのは使用するバージョンです。
2025年4月1日現在、FastEnumの最新バージョンは .NET8以上をサポートしているため、Unityに導入できません。
Unityに導入できる最高バージョンは1.8.0になります。

ちなみに最新バージョンのFastEnumは、SourceGenerator対応などもしているため、今回測定した速度よりさらに速くすることも可能です。
（すごく個人的な意見ですが、SourceGeneratorはいいぞ。）

## 2. 測定検証
Unity周りでプログラムの速度検証をする場合、正確さという面でこれというものがない難しさがあります。
いちおう`Perfomance testing API`を使う測定事例が多いとは思いますので、こちらの記事でも記載していますが、これも詳しい方によるとあまり信用できないとか。
Unity環境ではありませんが、C# Pureプロジェクト環境でBenchmarkDotNetを使用して計測した結果がありますので、こちらの値を信用していただければと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1233990/18c9d3ed-051f-479b-a9ee-3d1fbecf1f81.png)

| ※単位は秒で統一                       | BenchmarkDotNet | Perfomance testing API（100万回実行） |
| ------------------------------ | --------------- | ------------------------------- |
| ToString()                     | 0.0000000045855 | 2.577                           |
| Dictionary : 通常                | 0.0000000016383 | 0.071                           |
| Dictionary : IEqualityComparer | 0.0000000014465 | 0.072                           |
| Dictionary : keyをintキャスト       | 0.0000000016237 | 0.072                           |
| List                           | 0.0000000000025 | 0.026                           |
| 配列                             | 0.0000000000025 | 0.018                           |
| FastEnum : GetEnumMemberValue  | 0.0000000001386 | 0.140                           |

# 3. 感想
この記事、全面修正前に `Stopwatch` クラスで雑に計測したときは、`ToString()`が最速だったはずですが...むしろベベですね...
自分の単なるメモだったこの記事ですが、思ったより多くの方に影響があったとのこと、ご迷惑をおかけしました。
うん、`ToString()` はマジでないな...

# 4. 検証コード
<details><summary>Unity検証コード</summary>

```cs:SameCase.cs
using System.Collections.Generic;
using System.Runtime.Serialization;
using FastEnumUtility;
using NUnit.Framework;
using Unity.PerformanceTesting;

public class SameCase
{
    [Test, Order(0), Performance]
    public void ToStringCase()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = Fruits.Apple.ToString();
        }
    }
    
    static readonly Dictionary<Fruits, string> dic = new Dictionary<Fruits, string>
    {
        { Fruits.Apple, "Apple" },
        { Fruits.Banana, "Banana" },
        { Fruits.Peach, "Peach" }
    };
    
    [Test, Order(1), Performance]
    public void DictionaryCase()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = dic[Fruits.Apple];
        }
    }
    
    static readonly Dictionary<Fruits, string> dic2 = new Dictionary<Fruits, string>(new FruitsEqualityComparer())
    {
        { Fruits.Apple, "Apple" },
        { Fruits.Banana, "Banana" },
        { Fruits.Peach, "Peach" }
    };
    
    [Test, Order(2), Performance]
    public void DictionaryCase2()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = dic2[Fruits.Apple];
        }
    }
    
    static readonly Dictionary<int, string> dic3 = new Dictionary<int, string>
    {
        { (int)Fruits.Apple, "Apple" },
        { (int)Fruits.Banana, "Banana" },
        { (int)Fruits.Peach, "Peach" }
    };
    
    [Test, Order(3), Performance]
    public void DictionaryCase3()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = dic3[(int)Fruits.Apple];
        }
    }
    
    static readonly List<string> list = new List<string>
    {
        "Apple",
        "Banana",
        "Peach"
    };
    
    [Test, Order(4), Performance]
    public void ListCase()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = list[(int)Fruits.Apple];
        }
    }
    
    static readonly string[] array = new string[3]
    {
        "Apple",
        "Banana",
        "Peach"
    };
    
    [Test, Order(5), Performance]
    public void ArrayCase()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = array[(int)Fruits.Apple];
        }
    }

    [Test, Order(6), Performance]
    public void FastEnumCase()
    {
        for (int i = 0; i < 10000000; i++)
        {
            string hoge = Fruits.Apple.GetEnumMemberValue();
        }
    }
}

enum Fruits
{
    [EnumMember(Value = "Apple")]
    Apple,
    [EnumMember(Value = "Banana")]
    Banana,
    [EnumMember(Value = "Peach")]
    Peach
}

class FruitsEqualityComparer : IEqualityComparer<Fruits>
{
    public bool Equals(Fruits x, Fruits y)
    {
        return (int)x == (int)y;
    }

    public int GetHashCode(Fruits obj)
    {
        return ((int)obj).GetHashCode();
    }
}
```

</details>

<details><summary>BenchmarkDotNet検証コード</summary>

```cs: Program.cs
using System.Collections.Generic;
using System.Runtime.Serialization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using FastEnumUtility;

internal class Program
{
    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<SameCase>();
    }
}

public class SameCase
{
    [Benchmark]
    public void ToStringCase()
    {
        string hoge = Fruits.Apple.ToString();
    }
    
    static readonly Dictionary<Fruits, string> dic = new Dictionary<Fruits, string>
    {
        { Fruits.Apple, "Apple" },
        { Fruits.Banana, "Banana" },
        { Fruits.Peach, "Peach" }
    };
    
    [Benchmark]
    public void DictionaryCase()
    {
        string hoge = dic[Fruits.Apple];
    }
    
    static readonly Dictionary<Fruits, string> dic2 = new Dictionary<Fruits, string>(new FruitsEqualityComparer())
    {
        { Fruits.Apple, "Apple" },
        { Fruits.Banana, "Banana" },
        { Fruits.Peach, "Peach" }
    };
    
    [Benchmark]
    public void DictionaryCase2()
    {
        string hoge = dic2[Fruits.Apple];
    }
    
    static readonly Dictionary<int, string> dic3 = new Dictionary<int, string>
    {
        { (int)Fruits.Apple, "Apple" },
        { (int)Fruits.Banana, "Banana" },
        { (int)Fruits.Peach, "Peach" }
    };
    
    [Benchmark]
    public void DictionaryCase3()
    {
        string hoge = dic3[(int)Fruits.Apple];
    }
    
    static readonly List<string> list = new List<string>
    {
        "Apple",
        "Banana",
        "Peach"
    };
    
    [Benchmark]
    public void ListCase()
    {
        string hoge = list[(int)Fruits.Apple];
    }
    
    static readonly string[] array = new string[3]
    {
        "Apple",
        "Banana",
        "Peach"
    };
    
    [Benchmark]
    public void ArrayCase()
    {
        string hoge = array[(int)Fruits.Apple];
    }

    [Benchmark]
    public void FastEnumCase()
    {
        string hoge = Fruits.Apple.GetEnumMemberValue();
    }
}

enum Fruits
{
    [EnumMember(Value = "Apple")]
    Apple,
    [EnumMember(Value = "Banana")]
    Banana,
    [EnumMember(Value = "Peach")]
    Peach
}

class FruitsEqualityComparer : IEqualityComparer<Fruits>
{
    public bool Equals(Fruits x, Fruits y)
    {
        return (int)x == (int)y;
    }

    public int GetHashCode(Fruits obj)
    {
        return ((int)obj).GetHashCode();
    }
}
```
</details>