※実質、.net - C# non-boxing conversion of generic enum to int? - Stack Overflow の紹介記事です。
ジェネリック型パラメータと Enum の相性は悪い。
型パラメータの制約として、Enum クラスを指定できない。
Enum に対する制約は、せいぜい下記のようにしかできない。(ゆるい制約だとstructだけの場合も)
[サンプル1 : Enum に対する制約]
問題は、型パラメータで受け取った Enum 値を int などにキャストする場合である。
[サンプル1]で value を int へキャストしようとすると、コンパイラに怒られる。
TEnum の制約では、int へ変換可能かどうかを判断できないためである。
ただし、この方法はコードの通りボックス化(ボクシング)→ボックス化解除(アンボクシング)であるため、あまりパフォーマンス的によろしくない。
※このコードを少し変更(Get メソッドの処理を静的コンストラクタに移動)。
Expression.ConvertChecked でキャストするだけのラムダ式を構築している。
Expression.ConvertChecked を使うことで、コンパイラのキャスト可能チェックをスルーできる。
ちなみにオーバーフロー例外を発生させたくない場合は Expression.Convert を使う。
戻り値を決め打ちする必要があるため、動的ラムダ式より汎用性が下がる。
その他の型に変換したい場合は、OpCodes を参照。
また、型パラメータとして渡すもの(ここでは Enum)は public である必要がある。
public でないと TypeAccessException が発生する。
[結果]
※コマンドプロンプトで実施(csc /optimize)
※10回の算術平均、単位は[s]
結果をまとめると下記。(左の方が高性能)
キャスト : 動的ラムダ式 ≒ 動的IL > object
イニシャルコスト : 動的IL > 動的ラムダ式
キャスト処理に関しては、ボックス化を回避することで 2 倍以上の性能になる。
動的ラムダ式と動的IL で差はほぼないため、
汎用性や型パラメータの public 制限を考慮すると動的ラムダ式、イニシャルコストが少ない方がいいなら動的IL となる。
また、動的IL は拡張メソッドから呼び出すと、わずかだがその分のコストがかかる。
ただ、イニシャルコスト >>> object キャスト 1 回 なので、条件によっては無意味な工夫になる。
Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)
ジェネリック型パラメータと Enum の相性は悪い。
型パラメータの制約として、Enum クラスを指定できない。
Enum に対する制約は、せいぜい下記のようにしかできない。(ゆるい制約だとstructだけの場合も)
[サンプル1 : Enum に対する制約]
public static void HandleEnum<TEnum>(TEnum value)
where TEnum : struct, IComparable, IFormattable, IConvertible
{
}
問題は、型パラメータで受け取った Enum 値を int などにキャストする場合である。
[サンプル1]で value を int へキャストしようとすると、コンパイラに怒られる。
TEnum の制約では、int へ変換可能かどうかを判断できないためである。
object キャスト
これを解決する最も簡単な方法は、一度 object へキャストすることである。
public static void HandleEnum<TEnum>(TEnum value)
where TEnum : struct, IComparable, IFormattable, IConvertible
{
var i = (int)(object)value;
}
ただし、この方法はコードの通りボックス化(ボクシング)→ボックス化解除(アンボクシング)であるため、あまりパフォーマンス的によろしくない。
動的ラムダ式でキャスト
ボックス化を回避できる方法として、式木で動的ラムダ式を構築してキャストする方法がある。※このコードを少し変更(Get メソッドの処理を静的コンストラクタに移動)。
public static class CastTo<T>
{
private static class Cache<S>
{
static Cache()
{
// 次のラムダ式を式木で構築
// (S s) => (T)s
var p = Expression.Parameter(typeof(S));
var c = Expression.ConvertChecked(p, typeof(T));
Caster = Expression.Lambda<Func<S, T>>(c, p).Compile();
}
internal static readonly Func<S, T> Caster;
}
public static T From<S>(S source)
{
return Cache<S>.Caster(source);
}
}
[使用例]
public static void HandleEnum<TEnum>(TEnum value)
where TEnum : struct, IComparable, IFormattable, IConvertible
{
var i = CastTo<int>.From(value);
}
Expression.ConvertChecked でキャストするだけのラムダ式を構築している。
Expression.ConvertChecked を使うことで、コンパイラのキャスト可能チェックをスルーできる。
ちなみにオーバーフロー例外を発生させたくない場合は Expression.Convert を使う。
動的IL でキャスト
ここにもあるのだが、キャスト処理を動的IL で構築することで、ボックス化を回避できる。
public static class IlCastToInt32<T>
{
static IlCastToInt32()
{
var method = new DynamicMethod(
"IlCastToInt32Caster", typeof(int), new[] { typeof(T) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Conv_Ovf_I4);
il.Emit(OpCodes.Ret);
Caster = (Func<T, int>)method.CreateDelegate(typeof(Func<T, int>));
}
public static readonly Func<T, int> Caster;
}
// クラスの型パラメータを省略するための拡張メソッド
public static class IlCastExtensions
{
public static int CastToInt32<T>(this T value)
{
return IlCastToInt32<T>.Caster(value);
}
}
[使用例]
public static void HandleEnum<TEnum>(TEnum value)
where TEnum : struct, IComparable, IFormattable, IConvertible
{
var i = value.CastToInt32();
}
戻り値を決め打ちする必要があるため、動的ラムダ式より汎用性が下がる。
その他の型に変換したい場合は、OpCodes を参照。
また、型パラメータとして渡すもの(ここでは Enum)は public である必要がある。
public でないと TypeAccessException が発生する。
パフォーマンス
[測定コード]
static class TestEnumCast
{
static void Main(string[] args)
{
var flag = int.Parse(args[0]); // 1 ~ 5
var loopCount = int.Parse(args[1]);
var source = MuseEnumerable().Take(loopCount).ToArray();
var temp = new List<int>(loopCount);
if( flag > 3 ){
// 4,5 はイニシャルコスト無しの設定
// -> 一度実行してキャッシュを作成しておく
flag -= 2;
CastTo<int>.From(Muse.None);
Muse.None.CastToInt32();
}
var sw = new Stopwatch();
sw.Start();
switch( flag ){
case 1:
foreach( var item in source ) temp.Add((int)(object)item);
break;
case 2:
foreach( var item in source ) temp.Add(CastTo<int>.From(item));
break;
case 3:
// 拡張メソッドだとメソッド 1 個分のコストがかかるため、直呼び
foreach( var item in source ) temp.Add(IlCastToInt32<Muse>.Caster(item));
break;
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
static IEnumerable<Muse> MuseEnumerable()
{
while( true ){
yield return Muse.Honoka;
yield return Muse.Kotori;
yield return Muse.Umi;
}
}
}
[Flags]
public enum Muse
{
None = 0x0,
Honoka = 0x1,
Kotori = 0x2,
Umi = 0x4
}
[結果]
loopCount | object | 動的ラムダ式 | 動的IL | |
---|---|---|---|---|
イニシャルコスト有 | 10000 | 0.0003435 | 0.0053873 | 0.0031249 |
イニシャルコスト無 | 0.0001409 | 0.0001416 | ||
イニシャルコスト有 | 100000 | 0.0035345 | 0.0067660 | 0.0044378 |
イニシャルコスト無 | 0.0014422 | 0.0014731 |
※10回の算術平均、単位は[s]
結果をまとめると下記。(左の方が高性能)
キャスト : 動的ラムダ式 ≒ 動的IL > object
イニシャルコスト : 動的IL > 動的ラムダ式
キャスト処理に関しては、ボックス化を回避することで 2 倍以上の性能になる。
動的ラムダ式と動的IL で差はほぼないため、
汎用性や型パラメータの public 制限を考慮すると動的ラムダ式、イニシャルコストが少ない方がいいなら動的IL となる。
また、動的IL は拡張メソッドから呼び出すと、わずかだがその分のコストがかかる。
ただ、イニシャルコスト >>> object キャスト 1 回 なので、条件によっては無意味な工夫になる。
検証環境
Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0Intel(R) Celeron(R) CPU G530 @ 2.40GHz/DDR3 8GB(4GB x 2)
0 件のコメント:
コメントを投稿