更新
- 2024年2月4日 投稿
- 2025年4月1日 指摘を受け、正確なパフォーマンス検証記事となるよう全面修正
0. この記事は?
もう、タイトル通り。
列挙体は、デフォでは(数字はできても)文字列にはキャストできないので、ではどんな実装ならばパフォーマンスがいいのか、調べて考察するというもの。
1. 思いつく実装
とりあえず、パフォーマンス比較対象になりうる実装を挙げていきます。
なお、以下のような列挙体をキャストすると思ってください。
using System.Runtime.Serialization;
public enum Fruits
{
[EnumMember(Value = "Apple")]
Apple,
[EnumMember(Value = "Banana")]
Banana,
[EnumMember(Value = "Peach")]
Peach
}
1-1. 列挙体をそのまま ToString()
しちゃう
string hoge = Fruits.Apple.ToString();
一番定番で、一番手軽だと思います。
こちら、機能的なデメリットとして、例えば "Main Window"
みたいな半角スペースを入れた文字列などを返してほしいケースに対応できません。
パフォーマンス面も、毎回 ToString()
を呼び出すたびに文字列を生成するのは、ちょっとよろしくありません。
この記事は速度検証がメインですが、メモリを話をするなら、これが一番よくないかも。
1-2. 連想配列を使うパターン
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くらいで速度改善されていたはず。
なので、上記実装でもそんなに遅くなかったはずですが、念のため対策したものも検証しておきましょう。
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>
を実装している型にキャストすればいいのでは、という案もあると思います。
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>
は内部でハッシュテーブルアルゴリズムを利用した挙動をするわけですが、シンプルな配列やリストの方が速いのでは?という案もあるかと思います。
これも検証しておきましょう。
static readonly List<string> list = new List<string>
{
"Apple",
"Banana",
"Peach"
};
string hoge = list[(int)Fruits.Apple];
static readonly string[] array = new string[3]
{
"Apple",
"Banana",
"Peach"
};
string hoge = array[(int)Fruits.Apple];
ここらへんの実装は、正直書き心地や可読性考えると、到底利用できる選択肢ではない気がします。
ただ理論値ここらへんが最速なのは間違いない。
1-4. FastEnumを使う
ちょっと派生して、ライブラリを使った実装の計測もしてみたいと思います。
C#の列挙体周りのライブラリで名高い「FastEnum」を使い、事前に System.Runtime.Serialization.EnumMemberAttribute
を使った列挙体定義をしていると、かなり書き心地よく、以下のような実装が書けます。
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を使用して計測した結果がありますので、こちらの値を信用していただければと思います。
※単位は秒で統一 | 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. 検証コード
Unity検証コード
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();
}
}
BenchmarkDotNet検証コード
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();
}
}