EnumのToStringが遅いらしいので式木の力を借りた

  • 31
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

EnumオブジェクトのToStringメソッドはswitch文の100倍以上遅いのでILGeneratorで動的にswitch文を生成&コンパイルして高速化する方法によると、EnumのTostringは、内部でリフレクションしているため、とても遅いらしい。

ILGeneratorじゃなくてExpressionTreeで何とかできるのではないか?

なんとかできた。
ビット演算を行うため、Enumの値をInt32にキャストしています。
そのため、Int64形式のEnumだとうまく行かない可能性があるかも知れません。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Sample {
    public static class EnumToStringHelper {
        class cache<T> where T : struct {
            public static Func<T , string> ToString {
                get;
            } = toStringBuilder<T>();
        }

        private readonly static MethodInfo append = typeof( StringBuilder ).GetMethod( nameof( StringBuilder.Append ) , new[] { typeof( string ) } );
        private readonly static MethodInfo toString = typeof( StringBuilder ).GetMethod( nameof( StringBuilder.ToString ) , Type.EmptyTypes );

        private static Func<T , string> toStringBuilder<T>() where T : struct {
            var valueParam = Expression.Parameter( typeof( T ) );
            var valueInt = Expression.Variable( typeof( int ) );
            var buffer = Expression.Variable( typeof( StringBuilder ) );
            var flags = typeof( T ).GetTypeInfo().IsDefined( typeof( FlagsAttribute ) );

            var separator =
                    Expression.IfThen(
                        Expression.LessThan( Expression.Constant( 0 , typeof( int ) ) , Expression.Property( buffer , nameof( StringBuilder.Length ) ) ) ,
                        Expression.Call( buffer , append , Expression.Constant( ", " , typeof( string ) ) )
                    );


            var members = ( (T[])Enum.GetValues( typeof( T ) ) )
                .Distinct()
                .Select( x =>
                {
                    var value = Convert.ToInt32( x );
                    var flagValue = Expression.Constant( value );
                    var label = (Expression)Expression.Constant( x.ToString() , typeof( string ) );

                    return new
                    {
                        Value = value ,
                        Expression = value == 0 ?
                            label :
                            Expression.IfThen(
                                Expression.Equal( flagValue , flags ? (Expression)Expression.And( flagValue , valueInt ) : valueInt ) ,
                                Expression.Block(
                                    separator ,
                                    Expression.Call( buffer , append , label )
                                )
                            )
                    };
                } )
                .ToArray();

            return Expression.Lambda<Func<T , string>>(
                    Expression.Block(
                        new[] { buffer , valueInt } ,
                        Expression.Assign( buffer , Expression.New( typeof( StringBuilder ) ) ) ,
                        Expression.Assign( valueInt , Expression.Convert( valueParam , typeof( int ) ) ) ,
                        Expression.IfThenElse(
                            Expression.Equal( valueInt , Expression.Constant( 0 , typeof( int ) ) ) ,
                            Expression.Call( buffer , append , ( members.FirstOrDefault( x => x.Value == 0 )?.Expression ) ?? (Expression)Expression.Constant( "0" , typeof(string) ) ) ,
                            Expression.Block( members.Where( x => x.Value != 0 ).Select( x => x.Expression ) )
                        ) ,
                        Expression.Call( buffer , toString )
                    ) ,
                    valueParam
            )
            .Compile();

        }


        public static string EnumToString<T>( this T value ) where T : struct => cache<T>.ToString( value );
    }
}
使い方

使い方は、とても簡単です。ToStringの代わりにEnumToStringを呼ぶだけです。
A | B のような値も問題ありません。通常のToStringと同様の結果が得られるはずです。

public enum ValueTypes {
    None = 0,
    A = 1,
    B = 2,
    C = 4
}

static void Main( string[] args ) {
    Console.WriteLine( ValueTypes.None.EnumToString() ); // 0
    Console.WriteLine( ValueTypes.A.EnumToString() );    // A
    Console.WriteLine( ValueTypes.B.EnumToString() );    // B
    Console.WriteLine( ValueTypes.C.EnumToString() );    // C
    Console.WriteLine( ( ValueTypes.A | ValueTypes.B ).EnumToString() );     // A, B
    Console.WriteLine( ( ValueTypes.B | ValueTypes.C ).EnumToString() );     // B, C
    Console.WriteLine( ( ValueTypes.None | ValueTypes.C ).EnumToString() );  // C
    Console.WriteLine( ( ValueTypes.A | ValueTypes.B | ValueTypes.C ).EnumToString() ); // A, B, C
    Console.ReadLine();
}

// 出力
None
A
B
C
A, B
B, C
C
A, B, C
どれだけ速くなったか?

通常のToStringとの速度を比較してみます。

var sw = new Stopwatch();
sw.Start();
for( int i = 0; i < 1000000; i++ )
    GC.KeepAlive( ( ValueTypes.A | ValueTypes.C ).ToString() );
sw.Stop();
Console.WriteLine( sw.Elapsed );


sw.Restart();
for( int i = 0; i < 1000000; i++ )
    GC.KeepAlive( ( ValueTypes.A | ValueTypes.C ).EnumToString() );
sw.Stop();
Console.WriteLine( sw.Elapsed );
00:00:04.5814417
00:00:00.2161777

明らかに通常のTostringが遅いかわかります。

おまけ:任意の文字列に置き換える

Enumのフィールド名をそのまま使うより、ユーザーにわかりやすく別名で出力したい場合があります。

LabelAttributeクラスの定義
[AttributeUsage( AttributeTargets.Field )]
public sealed class LabelAttribute : Attribute {
    public LabelAttribute( string label ) {
        this.Value = label;
    }

    public string Value {
        get;
    }
}

それぞれのメンバーにLabel属性を付随させることで、別名を与えるようにします。

Label属性を付随する
public enum ValueTypes {
   [Label("ラベルA")]
   A
   [Label("ラベルB")]
   B
   [Label("ラベルC")]
   C
}

toStringBuilderメソッドの変更

変更前
var label = (Expression)Expression.Constant( x.ToString() , typeof( string ) );
変更後
 var label = (Expression)Expression.Constant( typeof( T ).GetField( x.ToString() ).GetCustomAttribute<LabelAttribute>()?.Value ?? x.ToString() , typeof( string ) );