C#
enum
performance
ilgenerator

EnumオブジェクトのToStringメソッドはswitch文の100倍以上遅いのでILGeneratorで動的にswitch文を生成&コンパイルして高速化する方法

More than 3 years have passed since last update.


1.Enum.ToStringの内部実装とよくある拡張メソッド

EnumのToStringは非常に遅いです。


DayOfWeek.ToStirng()

var dw = DayOfWeek.Sunday;

var dwText = dw.ToString();

これは内部ではリフレクションを使用しているためです。


Enum.ToString()

public override string ToString()

{
Type type = base.GetType();
object obj2 = ((RtFieldInfo)GetValueField(type)).InternalGetValue(this, false);
return InternalFormat(type, obj2);
}

参考サイト→http://www.dotnetperls.com/enum-tostring

早くするためにはこんな感じで拡張メソッドを定義してあげるとよいでしょう。


EnumExtensions.ToString()


public static class EnumEx
{
public static String ToStringFromEnum(this DayOfWeek value)
{
switch (value)
{
case DayOfWeek.Friday: return "Friday";
case DayOfWeek.Monday: return "Monday";
//…省略
default: throw new InvalidOperationException();
}
}
}


だいたい100倍以上スピードが早くなります。

これをあなたが使う全てのEnumに定義してあげれば問題無しです。


2.全部定義するのは大変→ILGeneratorを利用する

拡張メソッドを全てのEnumに定義していては時間がかかりすぎてしまいます。簡単に思いつく方法としてはT4やコードスニペットなどで自動生成というのが思いつきます。ただこれらの自動生成の方法はEnumに変更があったときに再度生成処理をしないといけないので保守性が完璧ではありません。ここではさらに一歩先の方法として「完璧な保守性とパフォーマンスを両立する方法」を紹介します。

.NETではILGeneratorクラスというのがあり、プログラムの実行中にILコードを動的にコンパイルして利用することが可能です。今回は動的にFunc<Enum, String>メソッドを生成し、そのメソッドを呼び出すという方法を紹介します。


3.まずはDayOfWeekで動作するコードを生成する

これから上記のswitch文のロジックのメソッドを動的に作成する方法を紹介します。ILGeneratorオブジェクトを使ってILコードを直接記述しメソッドを作成するメソッドを作成します。簡単にコメントを入れておきましたのでそこそこプログラムがわかる人なら理解できると思います。


CreateToStringFromEnumFunc()


private static Func<T, String> CreateToStringFromEnumFunc<T>()
{
var tp = typeof(T);
DynamicMethod dm = new DynamicMethod("ToStringFromEnum", typeof(String), new[] { tp });
ILGenerator il = dm.GetILGenerator();
Label defaultCase = il.DefineLabel();

var names = Enum.GetNames(tp);
//各曜日分のラベルとdefaultラベルを作成
var caseLabels = new Label[names.Length + 1];
for (int i = 0; i < names.Length; i++)
{
caseLabels[i] = il.DefineLabel();
}
caseLabels[names.Length] = defaultCase;

il.Emit(OpCodes.Ldarg_0);//引数として渡されるDayOfWeekの値をスタックにロード
il.Emit(OpCodes.Switch, caseLabels);//その値を元にswitchで各ラベルへ分岐

for (int i = 0; i < names.Length; i++)
{
// Case ??: return "";の部分になります。
il.MarkLabel(caseLabels[i]);//各曜日のラベルを定義。switchからここへ飛んでくる
il.Emit(OpCodes.Ldstr, names[i]);//曜日の文字列("Sunday"とか)をスタックにロード
il.Emit(OpCodes.Ret);//ロードした値を呼び出し元へ戻り値として戻す
}
//この2行が default: throw new InvalidOperationExceptionになります。
il.MarkLabel(defaultCase);
il.ThrowException(typeof(InvalidOperationException));

var f = typeof(Func<,>);
var gf = f.MakeGenericType(tp, typeof(String));//Generic引数を定義
return (Func<T, String>)dm.CreateDelegate(gf);//作成したILコードのロジックを持つメソッドを作成する
}


このメソッドはEnum型を受け取って動的にメソッド(Func<T, String>)を生成してくれます。例えばDayOfWeekを渡すとFunc<DayOfWeek, String>型のメソッドを作ってくれます。


CreateDayOfWeekMethod()

Func<DayOfWeek, String> md = CreateToStringFromEnumFunc<DayOfWeek>();


この時に生成されるILのコードはこんな感じになるはずです。


ILCode


.method public hidebysig instance string
ToString(valuetype [mscorlib]System.DayOfWeek 'value') cil managed
{
// コード サイズ 101 (0x65)
.maxstack 1
.locals init ([0] valuetype [mscorlib]System.DayOfWeek V_0,
[1] string V_1)
IL_0000: nop
IL_0001: ldarg.1
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: switch (
IL_0027,
IL_002f,
IL_0037,
IL_003f,
IL_0047,
IL_004f,
IL_0057)
IL_0025: br.s IL_005f
IL_0027: ldstr "Sunday"
IL_002c: stloc.1
IL_002d: br.s IL_0063
IL_002f: ldstr "Monday"
IL_0034: stloc.1
IL_0035: br.s IL_0063
IL_0037: ldstr "Tuesday"
IL_003c: stloc.1
IL_003d: br.s IL_0063
IL_003f: ldstr "Wednesday"
IL_0044: stloc.1
IL_0045: br.s IL_0063
IL_0047: ldstr "Thursday"
IL_004c: stloc.1
IL_004d: br.s IL_0063
IL_004f: ldstr "Friday"
IL_0054: stloc.1
IL_0055: br.s IL_0063
IL_0057: ldstr "Saturday"
IL_005c: stloc.1
IL_005d: br.s IL_0063
IL_005f: ldnull
IL_0060: stloc.1
IL_0061: br.s IL_0063
IL_0063: ldloc.1
IL_0064: ret
}


C#で表現するとこんな感じになっているでしょう。


md


String md(DayOfWeek value)
{
switch (value)
{
case DayOfWeek.Friday: return "Friday";
case DayOfWeek.Monday: return "Monday";
//…省略
default: throw new InvalidOperationException();
}
}

このようにILGeneratorを使用すれば動的にメソッドを作成することが可能になります。


4.特殊な定義がされたEnumでも動作するように改良する

しかしこのCreateToStringFromEnumFuncメソッドにはバグがあります。例えば下のようなEnumの場合はうまく動きません。

1.最初の値が0以外

2.型がInt32以外(Byte,Int64など)

3.飛び飛びの値

4.範囲外の値が来た場合→((DayOfWeek)(-1))など

というような定義がされたEnumの場合はうまく動作しません。

public enum BaseIsNotZeroEnum : byte
{
V2 = 2,
V3,
V4,
V5,
V6,
}
public enum ByteEnum : byte
{
V0 = 0,
V2 = 2,
V4 = 4,
}
public enum LongEnum : long
{
V0 = 0,
V2 = 2,
V1620100 = 1620100,
V23372036854775807 = 23372036854775807,
}

こういったEnumにも対応できるようメソッドを作り直します。0から始まり連続値を持つEnumの場合はswitch文、それ以外の場合はif文で処理をするように改良します。また範囲外の値が指定された場合は例外を投げるように修正します。


CreateToStringFromEnumFunc()


private static Func<T, String> CreateToStringFromEnumFunc<T>()
{
var tp = typeof(T);
DynamicMethod dm = new DynamicMethod("ToStringFromEnum", typeof(String), new[] { tp });
ILGenerator il = dm.GetILGenerator();

var values = ((T[])Enum.GetValues(tp)).Select(el => Convert.ToInt64(el)).ToList();
var names = Enum.GetNames(tp);

var returnLabel = il.DefineLabel();
//Have any value different from index number
if (values.Where((el, i) => el != i).Any())
{
var result = il.DeclareLocal(typeof(String));

for (int i = 0; i < values.Count; i++)
{
//引数として渡されたEnumの値をスタックへ置く
il.Emit(OpCodes.Ldarg_0);
//Int64(Conv_I8)へコンバート
il.Emit(OpCodes.Conv_I8);
//i番目のEnumの値をInt64でスタックへ置く
il.Emit(OpCodes.Ldc_I8, values[i]);
//スタックへ置いた二つの値を比較
il.Emit(OpCodes.Ceq);

var label = il.DefineLabel();
il.Emit(OpCodes.Brfalse, label);//比較してfalseの場合はこの4行下のil.MarkLabel(label);までジャンプ

//値が一致した場合、ローカル変数に保存してil.MarkLabel(returnLabel);へジャンプ
il.Emit(OpCodes.Ldstr, names[i]);
il.Emit(OpCodes.Stloc, result);
il.Emit(OpCodes.Br, returnLabel);

il.MarkLabel(label);
}
//どのif文にもマッチしない→範囲外の値ということで例外を投げる
il.ThrowException(typeof(InvalidOperationException));

il.MarkLabel(returnLabel);
il.Emit(OpCodes.Ldloc, result);//保存した値を呼び出して
il.Emit(OpCodes.Ret);//戻り値として返す
}
else
{
//if (arg1 < 0) ならil.MarkLabel(returnLabel);へジャンプ
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Ldc_I4, 0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Clt);//ltはLess Thanの略。val1 < val2
il.Emit(OpCodes.Brtrue, returnLabel);
//if (arg1 > Enum.GetValues().Length - 1) ならil.MarkLabel(returnLabel);へジャンプ
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Ldc_I4, names.Length - 1);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Cgt);//gtはGreater Thanの略。val1 > val2
il.Emit(OpCodes.Brtrue, returnLabel);

//ここからswitch文開始
il.Emit(OpCodes.Ldarg_0);
var caseLabels = new Label[names.Length + 1];
for (int i = 0; i < names.Length; i++)
{
caseLabels[i] = il.DefineLabel();
}
Label defaultCase = il.DefineLabel();
caseLabels[names.Length] = defaultCase;
il.Emit(OpCodes.Switch, caseLabels);
for (int i = 0; i < names.Length; i++)
{
// Case ??: return "";
il.MarkLabel(caseLabels[i]);
il.Emit(OpCodes.Ldstr, names[i]);
il.Emit(OpCodes.Ret);
}
il.MarkLabel(defaultCase);
il.ThrowException(typeof(InvalidOperationException));

il.MarkLabel(returnLabel);
il.ThrowException(typeof(InvalidOperationException));
}

var f = typeof(Func<,>);
var gf = f.MakeGenericType(tp, typeof(String));
return (Func<T, String>)dm.CreateDelegate(gf);
}


これできちんと動作するようになりました。


5.全てのEnumに対して利用可能なToStringFromEnum拡張メソッドを作成する

仕上げです。生成処理自体はそこそこ重たい処理になります。生成してできあがったメソッドは不変(何回生成しても結果は一緒)なのでDictionaryにでも保存しておきましょう。全部をまとめたクラスは以下のようになります。WEBなどで複数のスレッドから同時に呼ばれても大丈夫なようにConcurrentDictionaryを使用します。


EnumExtensions.cs


public static class EnumExtensions
{
private static ConcurrentDictionary<Type, MulticastDelegate> _ToStringFromEnumMethods = new ConcurrentDictionary<Type, MulticastDelegate>();

public static String ToStringOrNullFromEnum<T>(this Nullable<T> value)
where T : struct
{
if (value.HasValue == true) return ToStringFromEnum(value.Value);
return null;
}
public static String ToStringFromEnum<T>(this Nullable<T> value)
where T : struct
{
if (value.HasValue == true) return ToStringFromEnum(value.Value);
return "";
}
public static String ToStringFromEnum<T>(this T value)
where T : struct
{
var tp = typeof(T);
if (tp.IsEnum == false) throw new ArgumentException("value must be a enum type");

MulticastDelegate md = null;
if (_ToStringFromEnumMethods.TryGetValue(tp, out md) == false)
{
var aa = tp.GetCustomAttributes(typeof(FlagsAttribute), false);
if (aa.Length == 0)
{
md = CreateToStringFromEnumFunc<T>();
}
_ToStringFromEnumMethods[tp] = md;
}
// Flags
if (md == null) return value.ToString().Replace(" ", "");

var f = (Func<T, String>)md;
return f(value);
}
private static Func<T, String> CreateToStringFromEnumFunc<T>()
{
var tp = typeof(T);
DynamicMethod dm = new DynamicMethod("ToStringFromEnum", typeof(String), new[] { tp });
ILGenerator il = dm.GetILGenerator();

var values = ((T[])Enum.GetValues(tp)).Select(el => Convert.ToInt64(el)).ToList();
var names = Enum.GetNames(tp);

var returnLabel = il.DefineLabel();
//Have any value different from index number
if (values.Where((el, i) => el != i).Any())
{
var result = il.DeclareLocal(typeof(String));

for (int i = 0; i < values.Count; i++)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Ldc_I8, values[i]);
il.Emit(OpCodes.Ceq);

var label = il.DefineLabel();
il.Emit(OpCodes.Brfalse, label);

il.Emit(OpCodes.Ldstr, names[i]);
il.Emit(OpCodes.Stloc, result);
il.Emit(OpCodes.Br, returnLabel);

il.MarkLabel(label);
}
il.ThrowException(typeof(InvalidOperationException));

il.MarkLabel(returnLabel);
il.Emit(OpCodes.Ldloc, result);
il.Emit(OpCodes.Ret);
}
else
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Ldc_I4, 0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Clt);
il.Emit(OpCodes.Brtrue, returnLabel);

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Ldc_I4, names.Length - 1);
il.Emit(OpCodes.Conv_I8);
il.Emit(OpCodes.Cgt);
il.Emit(OpCodes.Brtrue, returnLabel);

il.Emit(OpCodes.Ldarg_0);
var caseLabels = new Label[names.Length + 1];
for (int i = 0; i < names.Length; i++)
{
caseLabels[i] = il.DefineLabel();
}
Label defaultCase = il.DefineLabel();
caseLabels[names.Length] = defaultCase;
il.Emit(OpCodes.Switch, caseLabels);
for (int i = 0; i < names.Length; i++)
{
// Case ??: return "";
il.MarkLabel(caseLabels[i]);
il.Emit(OpCodes.Ldstr, names[i]);
il.Emit(OpCodes.Ret);
}
il.MarkLabel(defaultCase);
il.ThrowException(typeof(InvalidOperationException));

il.MarkLabel(returnLabel);
il.ThrowException(typeof(InvalidOperationException));
}

var f = typeof(Func<,>);
var gf = f.MakeGenericType(tp, typeof(String));
return (Func<T, String>)dm.CreateDelegate(gf);
}
}


※EnumがFlagsAttributeの場合は通常のToStringメソッドを呼び出すようにしておきました。これは

1.どう出力するか仕様が決定しづらい

→One,TwoなのかOne:Twoとコロンを使用するのかOne, Twoとスペースをいれるべきなのかなど。

2.上記を問題もありILコードの作成が非常に面倒

なのでやってないです。

従ってFlagsAttributeの時はパフォーマンスは通常と変わりません。パフォーマンスが気になるのならば別途各自で拡張メソッドなど定義するのがよいでしょう。


6.品質を確保するためにテストコードを作成する

コードの品質を保つためテストコードも作っておきましょう。


TestEnum.cs


[TestClass]
public class EnumExtensionsTest
{
public enum BaseIsNotZeroEnum : byte
{
V2 = 2,
V3,
V4,
V5,
V6,
}
public enum ByteEnum : byte
{
V0 = 0,
V2 = 2,
V4 = 4,
}
public enum LongEnum : long
{
V0 = 0,
V2 = 2,
V1620100 = 1620100,
V23372036854775807 = 23372036854775807,
}
[FlagsAttribute]
enum FlagsEnum
{
None = 0x00, // 0000 0000
One = 0x01, // 0000 0001
Two = 0x02, // 0000 0010
Three = 0x04, // 0000 0100
Four = 0x08, // 0000 1000
All = 0x0F // 0000 1111
}
[TestMethod]
public void BasicEnumTest()
{
Assert.AreEqual("Friday", DayOfWeek.Friday.ToStringFromEnum());
Assert.AreEqual("Wednesday", DayOfWeek.Wednesday.ToStringFromEnum());
Assert.AreEqual("Saturday", DayOfWeek.Saturday.ToStringFromEnum());
Assert.AreEqual("Friday", ((DayOfWeek)(5)).ToStringFromEnum());
Assert.AreEqual("Saturday", ((DayOfWeek)(6)).ToStringFromEnum());
}
[TestMethod]
public void BaseIsNotZeroEnumTest()
{
Assert.AreEqual("V2", BaseIsNotZeroEnum.V2.ToStringFromEnum());
Assert.AreEqual("V3", BaseIsNotZeroEnum.V3.ToStringFromEnum());
Assert.AreEqual("V4", BaseIsNotZeroEnum.V4.ToStringFromEnum());
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void EmptyValueTest()
{
String s = "";
s = ((DayOfWeek)(-1)).ToStringFromEnum();
s = ((DayOfWeek)(7)).ToStringFromEnum();
s = ((HttpStatusCode)(3)).ToStringFromEnum();
}
[TestMethod]
public void ByteEnumTest()
{
Assert.AreEqual("V0", ByteEnum.V0.ToStringFromEnum());
Assert.AreEqual("V2", ByteEnum.V2.ToStringFromEnum());
Assert.AreEqual("V4", ByteEnum.V4.ToStringFromEnum());
}
[TestMethod]
public void LongEnumTest()
{
Assert.AreEqual("V0", LongEnum.V0.ToStringFromEnum());
Assert.AreEqual("V2", LongEnum.V2.ToStringFromEnum());
Assert.AreEqual("V1620100", LongEnum.V1620100.ToStringFromEnum());
Assert.AreEqual("V23372036854775807", LongEnum.V23372036854775807.ToStringFromEnum());
}
[TestMethod]
public void FlagEnumTest()
{
Assert.AreEqual("One", FlagsEnum.One.ToStringFromEnum());
Assert.AreEqual("One,Two", (FlagsEnum.One | FlagsEnum.Two).ToStringFromEnum());
Assert.AreEqual("Three", FlagsEnum.Three.ToStringFromEnum());
Assert.AreEqual("Four", FlagsEnum.Four.ToStringFromEnum());
}
[TestMethod]
public void HttpStatusCodeEnumTest()
{
Assert.AreEqual("Continue", HttpStatusCode.Continue.ToStringFromEnum());
Assert.AreEqual("BadRequest", HttpStatusCode.BadRequest.ToStringFromEnum());
Assert.AreEqual("Forbidden", HttpStatusCode.Forbidden.ToStringFromEnum());
Assert.AreEqual("InternalServerError", HttpStatusCode.InternalServerError.ToStringFromEnum());
Assert.AreEqual("OK", ((HttpStatusCode)(200)).ToStringFromEnum());
}
}


という感じで比較的簡単にEnumのToStringメソッドの超高速かつ保守性にも優れた拡張メソッドができました。

ご利用はご自由にどうぞ。

※修正履歴

2015/12/08 HttpStatusCodeに対して実行すると例外が発生するバグがあったので修正。範囲外の値の場合は例外を投げるように修正。