こんにちは。開発のY.Mです。

.NET には「リフレクション」という機能があり、プログラムの実行中に型情報(型が持つプロパティやメソッドなど)や属性(Attribute)を取得しプログラムで利用する事ができます。
リフレクションについては説明だけで何本か記事が書けてしまうので、詳しくは .NET のリファレンスを参照してください。

.NET のリフレクション
https://learn.microsoft.com/ja-jp/dotnet/fundamentals/reflection/reflection

属性を使用したメタデータの拡張
https://learn.microsoft.com/ja-jp/dotnet/standard/attributes/

この記事は「.NET のリフレクション完全に理解した」という前提で進めていきます。
もちろん C# の基本的な文法も完全に理解している前提です。

 

リフレクションに足りないものは、それは……速さが足りない!!

さて、色々と便利に使えるリフレクションなのですが、処理速度が遅いという欠点があります。属性の取得は特に遅く、多用するとパフォーマンスの低下が引き起こされます。
リフレクションをループ処理中に使わないなど工夫次第で対処はできるのですが、細かい事は考えずガンガン使いたいのでリフレクション関連の処理を高速化しよう、というのが本記事の主旨となります。
ほぼコピペで使えるリフレクション高速化コードを用意しましたので、そちらをベースに解説していきます。

 

高速化のありがちで安易なやりかた

時間が掛かる処理の高速化には様々な手段がありますが、率直に思い付くのは「時間が掛かる処理を何度も実行しない」辺りでしょう。
リフレクション関連の情報はコンパイル時に確定していますので、処理条件が同じなら常に同じ結果が返されます(※)。初回に実行した処理の結果をキャッシュする(メモリ上に保持しておく)ことで、2回目以降はキャッシュから読み込んで処理を省くことができます。

※ dynamic 型は実行時に型情報が決まりますので常に同じ結果になるとは限りません。本記事において dynamic 型については考慮の対象外としておきます。

 

プロパティ情報の取得を高速化

早速ですが、こちらがプロパティ情報の取得を高速化するコードです。
C# 10.0 (.NET 6.0)以降対応です。

using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

namespace Extensions;

/// <summary>
/// プロパティ関連拡張機能
/// </summary>
public static class PropertyExtension
{
    /// <summary>
    /// プロパティ情報のリストを取得(オブジェクトを指定)
    /// </summary>
    /// <typeparam name="T">オブジェクトの型</typeparam>
    /// <param name="obj">オブジェクト</param>
    /// <param name="bindingAttr">プロパティ検索方法</param>
    /// <returns>プロパティ情報のリスト</returns>
    public static List<PropertyInfo> EmGetProperties<T>(this T obj, BindingFlags? bindingAttr = null)
        where T : class
    {
        // オブジェクトの型が object の場合はオブジェクトの実際の型を取得し Dictionary Caching から取得
        if (typeof(T) == typeof(object))
        {
            return PropertyCaches.GetProperties(obj.GetType(), bindingAttr);
        }

        return PropertyCache<T>.GetProperties(bindingAttr);
    }

    /// <summary>
    /// プロパティ情報のリストを取得(型情報指定)
    /// </summary>
    /// <param name="typ">型情報</param>
    /// <param name="bindingAttr">プロパティ検索方法</param>
    /// <returns>プロパティ情報のリスト</returns>
    public static List<PropertyInfo> EmGetProperties(this Type typ, BindingFlags? bindingAttr = null)
    {
        return PropertyCaches.GetProperties(typ, bindingAttr);
    }


    /// <summary>
    /// プロパティ情報取得(オブジェクト指定)
    /// </summary>
    /// <typeparam name="T">オブジェクトの型</typeparam>
    /// <param name="obj">オブジェクト</param>
    /// <param name="propName">プロパティ名</param>
    /// <param name="bindingAttr">プロパティ検索方法</param>
    /// <returns>プロパティ情報</returns>
    public static PropertyInfo? EmGetProperty<T>(this T obj, string propName, BindingFlags? bindingAttr = null)
        where T : class
    {
        // オブジェクトの型が object の場合はオブジェクトの実際の型を取得し Dictionary Caching から取得
        if (typeof(T) == typeof(object))
        {
            return PropertyCaches.GetProperties(obj.GetType(), bindingAttr).Find(x => x.Name == propName);
        }

        return PropertyCache<T>.GetProperties(bindingAttr).Find(x => x.Name == propName);
    }

    /// <summary>
    /// プロパティ情報取得(型情報指定)
    /// </summary>
    /// <param name="typ">型情報</param>
    /// <param name="propName">プロパティ名</param>
    /// <param name="bindingAttr">プロパティ検索方法</param>
    /// <returns>プロパティ情報</returns>
    public static PropertyInfo? EmGetProperty(this Type typ, string propName, BindingFlags? bindingAttr = null)
    {
        return PropertyCaches.GetProperties(typ, bindingAttr).Find(x => x.Name == propName);
    }



    /// <summary>
    /// プロパティ情報キャッシュクラス(Static Type Caching)
    /// </summary>
    /// <typeparam name="T">クラスの型</typeparam>
    private static class PropertyCache<T> where T : class
    {
        /// <summary> 前回のプロパティ検索方法 </summary>
        private static BindingFlags? previousBindingAttr = null;

        /// <summary> プロパティ情報キャッシュ </summary>
        private static List<PropertyInfo> properties;


        /// <summary>
        /// コンストラクタ
        /// </summary>
        static PropertyCache() => properties = typeof(T).GetProperties().ToList();

        /// <summary>
        /// プロパティリスト取得
        /// </summary>
        /// <param name="bindingAttr">プロパティ検索方法</param>
        /// <returns>プロパティリスト</returns>
        public static List<PropertyInfo> GetProperties(BindingFlags? bindingAttr = null)
        {
            // プロパティ検索方法が前回のプロパティ検索方法と異なる場合はプロパティリストを再取得する
            if (bindingAttr != previousBindingAttr)
            {
                if (bindingAttr is null)
                {
                    properties = typeof(T).GetProperties().ToList();
                }
                else
                {
                    properties = typeof(T).GetProperties((BindingFlags)bindingAttr).ToList();
                }

                previousBindingAttr = bindingAttr;
            }

            return properties;
        }
    }


    /// <summary>
    /// プロパティ情報キャッシュクラス(Dictionary Caching)
    /// </summary>
    private static class PropertyCaches
    {
        /// <summary> プロパティ情報キャッシュ </summary>
        private static readonly ConcurrentDictionary<string, PropertyCachesItem> properties = new();


        /// <summary>
        /// プロパティ情報リスト取得
        /// </summary>
        /// <param name="typ">型情報</param>
        /// <param name="bindingAttr">プロパティ検索方法</param>
        /// <returns>プロパティ情報リスト</returns>
        public static List<PropertyInfo> GetProperties(Type typ, BindingFlags? bindingAttr)
            => PropertyCachesHelper.GetProperties(properties, typ, bindingAttr);
    }


    /// <summary>
    /// プロパティ情報キャッシュアイテム
    /// </summary>
    private struct PropertyCachesItem
    {
        /// <summary> プロパティ情報リスト </summary>
        public List<PropertyInfo> Properties;

        /// <summary> プロパティ検索方法 </summary>
        public BindingFlags? BindingAttr;
    }


    /// <summary>
    /// プロパティ情報キャッシュヘルパー
    /// </summary>
    private static class PropertyCachesHelper
    {
        /// <summary>
        /// プロパティ情報リスト取得
        /// </summary>
        /// <param name="cache">キャッシュ格納先</param>
        /// <param name="typ">型情報</param>
        /// <param name="bindingAttr">プロパティ検索方法</param>
        /// <returns></returns>
        public static List<PropertyInfo> GetProperties(
            ConcurrentDictionary<string, PropertyCachesItem> cache,
            Type typ, BindingFlags? bindingAttr)
        {
            // プロパティ情報リスト取得ローカル関数
            static List<PropertyInfo> getProperties(Type typ, BindingFlags? bindingAttr)
            {
                if (bindingAttr is null)
                {
                    return typ.GetProperties().ToList();
                }
                else
                {
                    return typ.GetProperties((BindingFlags)bindingAttr).ToList();
                }
            }


            // 型情報がnull許容型の場合は本来の型情報を取得
            typ = Nullable.GetUnderlyingType(typ) ?? typ;

            // 型情報の FullName が未定義の場合はキャッシュから読み込まない
            if (string.IsNullOrWhiteSpace(typ.FullName))
            {
                return getProperties(typ, bindingAttr);
            }

            // キー値取得
            var key = typ.FullName;

            // プロパティ情報キャッシュからプロパティ情報リストの取得を試みる
            if (cache.TryGetValue(key, out var val) && val.BindingAttr == bindingAttr)
            {
                return val.Properties;
            }

            // プロパティ情報キャッシュからプロパティ情報リストを取得できなかった場合は
            // プロパティ情報リストを取得する
            var result = getProperties(typ, bindingAttr);

            // プロパティ情報キャッシュにプロパティ情報リストを追加もしくは更新する
            var item = new PropertyCachesItem() { Properties = result, BindingAttr = bindingAttr };
            cache.AddOrUpdate(key, item, (key, oldValue) => item);

            return result;
        }
    }
}

扱いやすいように拡張メソッド(Extension Methods)として実装しています。メソッド名の先頭に「Em」を付けて標準のメソッド名と競合しないようにし、一目で拡張メソッドであることがわかるようにしています。

こんな感じで使います。

// (1) オブジェクトの拡張メソッドとして使用
var obj = new ExampleClass();
var props = obj.EmGetProperties();

// ジェネリックの型指定を省略せずに書くとこうなる(引数から型推論できるので省略が可能)
var props = obj.EmGetProperties<ExampleClass>();


// (2) 型情報(Type)の拡張メソッドとして使用 その1
var props = typeof(ExampleClass).EmGetProperties();


// (3) 型情報(Type)の拡張メソッドとして使用 その2
var obj = new ExampleClass();
var props = obj3.GetType().EmGetProperties();

EmGetProperty メソッドの方はプロパティ名を指定して単一のプロパティ情報を取得します。

var obj = new ExampleClass();
var prop = obj.EmGetProperty(nameof(ExampleClass.Value1));

// こっちの書き方でも可
var prop = obj.EmGetProperty(nameof(obj.Value1));

 

高速化コードではプロパティを列挙した結果をキャッシュに保持しています。
キャッシュの保持は2パターンです。

(1)コンパイル時にリフレクション対象の型を特定できる場合

Static Type Caching という手法でキャッシュします。実装としては static なジェネリッククラス PropertyCache<T> を作り、キャッシュ対象の型をジェネリックに指定します。
キャッシュの取得は静的コンストラクターで行っています。静的コンストラクターはクラスの初期化時(static クラスの場合はメンバーに初めてアクセスする際)に一度だけ呼ばれる処理で、初回はキャッシュ取得で時間がかかりますが2回目以降はキャッシュを読み込むため高速に処理できます。
使い方のサンプルコードでは(1)のパターンが該当します。

(2)コンパイル時にリフレクション対象の型を特定できない場合

引数でリフレクション対象の型が object で渡される場合など、コンパイル時にリフレクション対象の型が特定できない場合は Dictionary を使ってキャッシュします。
いわゆる Dictionary Caching で、型のフルネームを Dictionary のキーに、プロパティ情報のリストとプロパティの検索方法を値に保持します。
使い方のサンプルコードでは(2)、(3)のパターンが該当します。

 

ひとまずここで処理速度を比較してみましょう。
処理速度の計測には BenchmarkDotNet を使用し、.NET6.0 ~ 9.0 を対象としています。
ベンチマーク結果の数値は環境により異なりますので、概ねこのような傾向にあるものと捉えてください。

[ベンチマークコード]

[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
[MinColumn, MaxColumn]
public class BenchReflection
{
    private readonly ReflectionBenchmarkClass obj = new();
    private readonly PropertyInfo prop = typeof(ReflectionBenchmarkClass).GetProperty(nameof(ReflectionBenchmarkClass.String001))!;


    [Benchmark(Description = "プロパティ列挙:リフレクション")]
    public PropertyInfo[] GetProperties_Reflection()
    {
        return typ.GetProperties();
    }

    [Benchmark(Description = "プロパティ列挙:キャッシュ使用(Static Type Caching)")]
    public List<PropertyInfo> GetProperties_StaticTypeCaching()
    {
        return obj.EmGetProperties();
    }

    [Benchmark(Description = "プロパティ列挙:キャッシュ使用(Dictionary Caching)")]
    public List<PropertyInfo> GetProperties_DictionaryCaching()
    {
        return typ.EmGetProperties();
    }
}

ReflectionBenchmarkClass は適当に100個のプロパティを突っ込んだベンチマーク用のクラスです。

[DisplayName("リフレクションベンチマーク用クラス")]
public class ReflectionBenchmarkClass
{
[DisplayName("文字列001")] public string String001 { get; set; } = "string 001";
[DisplayName("文字列002")] public string String002 { get; set; } = "string 002";
[DisplayName("文字列003")] public string String003 { get; set; } = "string 003";
・
・
・
[DisplayName("文字列100")] public string String100 { get; set; } = "string 100";
}

 

[ベンチマーク結果]

Method Runtime Mean Error StdDev Min Max Gen0 Allocated
プロパティ列挙:リフレクション .NET 6.0 811.011 ns 4.9268 ns 4.3675 ns 804.938 ns 820.250 ns 0.0982 824 B
‘プロパティ列挙:キャッシュ使用(Static Type Caching)’ .NET 6.0 9.948 ns 0.0617 ns 0.0577 ns 9.887 ns 10.066 ns
‘プロパティ列挙:キャッシュ使用(Dictionary Caching)’ .NET 6.0 67.547 ns 0.5450 ns 0.5098 ns 66.948 ns 68.485 ns 0.0038 32 B
プロパティ列挙:リフレクション .NET 7.0 814.590 ns 6.9525 ns 6.1632 ns 804.436 ns 826.245 ns 0.0982 824 B
‘プロパティ列挙:キャッシュ使用(Static Type Caching)’ .NET 7.0 10.291 ns 0.1435 ns 0.1343 ns 10.162 ns 10.582 ns
‘プロパティ列挙:キャッシュ使用(Dictionary Caching)’ .NET 7.0 71.382 ns 1.4450 ns 1.5461 ns 69.585 ns 74.228 ns 0.0038 32 B
プロパティ列挙:リフレクション .NET 8.0 711.508 ns 4.3022 ns 3.3589 ns 707.043 ns 716.643 ns 0.0982 824 B
‘プロパティ列挙:キャッシュ使用(Static Type Caching)’ .NET 8.0 9.995 ns 0.0609 ns 0.0569 ns 9.884 ns 10.085 ns
‘プロパティ列挙:キャッシュ使用(Dictionary Caching)’ .NET 8.0 45.472 ns 0.5099 ns 0.3981 ns 45.068 ns 46.505 ns 0.0038 32 B
プロパティ列挙:リフレクション .NET 9.0 680.037 ns 1.3160 ns 1.0275 ns 678.078 ns 681.450 ns 0.0982 824 B
‘プロパティ列挙:キャッシュ使用(Static Type Caching)’ .NET 9.0 5.424 ns 0.0594 ns 0.0555 ns 5.348 ns 5.554 ns
‘プロパティ列挙:キャッシュ使用(Dictionary Caching)’ .NET 9.0 42.367 ns 0.1140 ns 0.0952 ns 42.302 ns 42.627 ns 0.0038 32 B

 

Static Type Caching の速さが際立っています。.NET9.0ではリフレクションと比べて驚異の約125倍赤い誰かも真っ青の速さです。速さの理由はキャッシュにアクセスする際にメモリの位置を直接参照するので余計な処理が一切挟まらないからです。実行時にヒープメモリ(Allocated の値)を使用しない所もいいですね。

Dictionary Caching も最大で約16倍速く、こちらの実装でも充分に高速化できています。

注意点として、双方とも static クラスを利用しており、クラスのメンバが保持しているデータは明示的に削除しない限りプロセスが終了するまでメモリ上に残り続けます。リフレクションのキャッシュ程度であれば容量は大きくないのでそれほど気にしなくていいように思います。

 

属性取得を高速化

お次は属性取得の高速化です。

using System.Collections.Concurrent;
using System.Reflection;

namespace Extensions;

/// <summary>
/// 属性関連拡張機能
/// </summary>
public static class AttributeExtension
{
    /// <summary>
    /// 属性のリストを取得
    /// </summary>
    /// <param name="info">メンバー情報</param>
    /// <param name="inherit">継承元属性取得フラグ</param>
    /// <returns>属性のリスト</returns>
    /// <remarks>対象に付加されている属性をすべて取得する。</remarks>
    public static List<Attribute> EmGetAttributes(this MemberInfo info, bool? inherit = null)
        => AttributeCache<Attribute>.GetAttributes(info, inherit);

    /// <summary>
    /// 属性のリスト取得(属性指定)
    /// </summary>
    /// <typeparam name="T">属性の型</typeparam>
    /// <param name="info">メンバー情報</param>
    /// <param name="inherit">継承元属性取得フラグ</param>
    /// <returns>属性のリスト</returns>
    /// <remarks>
    ///   対象から T で指定した属性をすべて取得する。<br/>
    ///   すべての属性を取得したい場合は T に Attribute を指定する。
    /// </remarks>
    public static List<T> EmGetAttributes<T>(this MemberInfo info, bool? inherit = null)
        where T : Attribute
        => AttributeCache<T>.GetAttributes(info, inherit);

    /// <summary>
    /// 属性を取得
    /// </summary>
    /// <typeparam name="T">属性の型</typeparam>
    /// <param name="info">メンバー情報</param>
    /// <param name="inherit">継承元属性取得フラグ</param>
    /// <returns>属性</returns>
    /// <remarks>対象から T で指定した属性で最初に見付かったものを取得する。</remarks>
    public static T? EmGetAttribute<T>(this MemberInfo info, bool? inherit = null)
        where T : Attribute
        => AttributeCache<T>.GetAttributes(info, inherit).FirstOrDefault();



    /// <summary>
    /// 属性キャッシュクラス
    /// </summary>
    /// <typeparam name="T">属性の型</typeparam>
    private static class AttributeCache<T> where T : Attribute
    {
        /// <summary> 属性キャッシュ </summary>
        private static readonly ConcurrentDictionary<string, AttributeCacheItem<T>> attribtes = new();

        /// <summary>
        /// 属性のリストを取得
        /// </summary>
        /// <param name="info">メンバー情報</param>
        /// <param name="inherit">継承元属性取得フラグ</param>
        /// <returns>属性のリスト</returns>
        public static List<T> GetAttributes(MemberInfo info, bool? inherit = null)
            => AttributeCacheHelper.GetAttributes(attribtes, info, inherit);
    }


    /// <summary>
    /// 属性キャッシュアイテム
    /// </summary>
    private struct AttributeCacheItem<T> where T : Attribute
    {
        /// <summary> 属性リスト </summary>
        public List<T> Attribtes;

        /// <summary> 継承元属性取得フラグ </summary>
        public bool? Inherit;
    }


    /// <summary>
    /// 属性キャッシュヘルパー
    /// </summary>
    private static class AttributeCacheHelper
    {
        /// <summary>
        /// 属性リスト取得
        /// </summary>
        /// <typeparam name="T">属性の型</typeparam>
        /// <param name="cache">キャッシュ格納先</param>
        /// <param name="info">メンバー情報</param>
        /// <param name="inherit">継承元属性取得フラグ</param>
        /// <returns>属性のリスト</returns>
        public static List<T> GetAttributes<T>(ConcurrentDictionary<string, AttributeCacheItem<T>> cache,
            MemberInfo info, bool? inherit = null) where T : Attribute
        {
            // ディクショナリのキー値取得
            string key = info switch
            {
                Type _info => _info.FullName ?? "",
                _ => $"{info.DeclaringType?.FullName ?? info.ReflectedType?.FullName ?? ""}.{info.Name}"
            };

            // キー値が取得できなかった場合は空のリストを返す
            if (key.Length <= 1) { return new(); }

            // 属性キャッシュから属性リストの取得を試みる
            if (cache.TryGetValue(key, out var val) && val.Inherit == inherit) { return val.Attribtes; }

            // 属性キャッシュから属性リストを取得できなかった場合は属性リストを取得
            List<T> result;
            if (inherit is null)
            {
                result = info.GetCustomAttributes<T>().ToList();
            }
            else
            {
                result = info.GetCustomAttributes<T>((bool)inherit).ToList();
            }

            // 属性キャッシュに属性リストを追加もしくは更新する
            var item = new AttributeCacheItem<T>() { Attribtes = result, Inherit = inherit };
            cache.AddOrUpdate(key, item, (key, oldValue) => item);

            return result;
        }
    }
}

キャッシュの保持方法は Static Type Caching と Dictionary Caching の合わせ技です。
注意点もプロパティ情報の取得と同じです。

こんな感じで使います。

var obj = new ExampleClass();

// クラスに付加されているすべての属性を取得
var attr1 = obj.GetType().EmGetAttributes();

// クラスに付加されているすべての Conditional 属性を取得 (Conditional 属性は複数付加できる)
var attr2 = obj.GetType().EmGetAttributes<ConditionalAttribute>();

// クラスに付加されている DisplayNameAttribute 属性を取得 (最初に見付かった1件)
var attr3 = obj.GetType().EmGetAttribute<DisplayNameAttribute>();


// プロパティに付加されているすべての属性を取得
var attr4 = obj.GetType().EmGetProperty(nameof(ExampleClass.Value1))?.EmGetAttributes();

// プロパティに付加されているすべての Conditional 属性を取得 (Conditional 属性は複数付加できる)
var attr5 = obj.GetType().EmGetProperty(nameof(ExampleClass.Value1))?.EmGetAttributes<ConditionalAttribute>();

// プロパティに付加されている DisplayNameAttribute 属性を取得 (最初に見付かった1件)
var attr6 = obj.GetType().EmGetProperty(nameof(ExampleClass.Value1))?.EmGetAttribute<DisplayNameAttribute>();

オブジェクトを生成しなくとも typeof で直接クラスを指定する事もできます。

var attr7 = typeof(ExampleClass).EmGetAttributes();
var attr8 = typeof(ExampleClass).EmGetAttributes<ConditionalAttribute>();
var attr9 = typeof(ExampleClass).EmGetAttribute<DisplayNameAttribute>();

var attr10 = typeof(ExampleClass).EmGetProperty(nameof(ExampleClass.Value1))?.EmGetAttributes();
var attr11 = typeof(ExampleClass).EmGetProperty(nameof(ExampleClass.Value1))?.EmGetAttributes<ConditionalAttribute>();
var attr12 = typeof(ExampleClass).EmGetProperty(nameof(ExampleClass.Value1))?.EmGetAttribute<DisplayNameAttribute>();

ベンチマークコードと結果はこちら。

[ベンチマークコード]

[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
[MinColumn, MaxColumn]
public class BenchReflection
{
    private readonly Type typ = typeof(ReflectionBenchmarkClass);
    private readonly PropertyInfo prop = typeof(ReflectionBenchmarkClass).GetProperty(nameof(ReflectionBenchmarkClass.String001))!;


    [Benchmark(Description = "クラス属性取得:リフレクション")]
    public DisplayNameAttribute? GetAttribute_Class_Reflection()
    {
        return typ.GetCustomAttributes<DisplayNameAttribute>().FirstOrDefault();
    }

    [Benchmark(Description = "クラス属性取得:キャッシュ使用")]
    public DisplayNameAttribute? GetAttribute_Class_DictionaryCaching()
    {
        return typ.EmGetAttribute<DisplayNameAttribute>();
    }


    [Benchmark(Description = "プロパティ属性取得:リフレクション")]
    public DisplayNameAttribute? GetAttribute_Prop_Reflection()
    {
        return prop.GetCustomAttributes<DisplayNameAttribute>().FirstOrDefault();
    }

    [Benchmark(Description = "プロパティ属性取得:キャッシュ使用")]
    public DisplayNameAttribute? GetAttribute_Prop_DictionaryCaching()
    {
        return prop.EmGetAttribute<DisplayNameAttribute>();
    }
}

[ベンチマーク結果]

Method Runtime Mean Error StdDev Min Max Gen0 Allocated
クラス属性取得:リフレクション .NET 6.0 1,047.620 ns 3.1472 ns 2.4571 ns 1,045.257 ns 1,054.535 ns 0.0343 296 B
クラス属性取得:キャッシュ使用 .NET 6.0 62.049 ns 0.4673 ns 0.4143 ns 61.521 ns 62.919 ns 0.0038 32 B
プロパティ属性取得:リフレクション .NET 6.0 818.975 ns 3.8818 ns 3.4411 ns 813.499 ns 825.306 ns 0.0277 232 B
プロパティ属性取得:キャッシュ使用 .NET 6.0 92.043 ns 1.7268 ns 1.6152 ns 89.616 ns 95.483 ns 0.0181 152 B
クラス属性取得:リフレクション .NET 7.0 929.055 ns 4.1874 ns 3.9169 ns 924.245 ns 938.023 ns 0.0362 304 B
クラス属性取得:キャッシュ使用 .NET 7.0 62.089 ns 0.1711 ns 0.1429 ns 61.860 ns 62.301 ns 0.0038 32 B
プロパティ属性取得:リフレクション .NET 7.0 723.178 ns 4.6640 ns 3.8947 ns 718.825 ns 732.476 ns 0.0286 240 B
プロパティ属性取得:キャッシュ使用 .NET 7.0 92.851 ns 0.9315 ns 0.7272 ns 91.797 ns 94.538 ns 0.0181 152 B
クラス属性取得:リフレクション .NET 8.0 920.130 ns 3.0480 ns 2.7020 ns 917.305 ns 925.289 ns 0.0362 304 B
クラス属性取得:キャッシュ使用 .NET 8.0 46.042 ns 0.6372 ns 0.5649 ns 45.452 ns 47.261 ns 0.0038 32 B
プロパティ属性取得:リフレクション .NET 8.0 733.177 ns 9.0103 ns 7.9874 ns 725.460 ns 753.628 ns 0.0286 240 B
プロパティ属性取得:キャッシュ使用 .NET 8.0 67.056 ns 0.7883 ns 0.7374 ns 66.107 ns 68.681 ns 0.0181 152 B
クラス属性取得:リフレクション .NET 9.0 903.911 ns 7.2421 ns 6.7742 ns 891.474 ns 917.612 ns 0.0362 304 B
クラス属性取得:キャッシュ使用 .NET 9.0 39.651 ns 0.8472 ns 1.0714 ns 38.234 ns 42.337 ns 0.0038 32 B
プロパティ属性取得:リフレクション .NET 9.0 668.055 ns 4.1793 ns 3.4899 ns 662.904 ns 674.978 ns 0.0286 240 B
プロパティ属性取得:キャッシュ使用 .NET 9.0 58.409 ns 0.5242 ns 0.4904 ns 57.142 ns 59.245 ns 0.0181 152 B

 

クラス属性取得でリフレクションと比べて最大約23倍、プロパティ属性取得で約11倍の高速化です。
属性は使い慣れてくると多用しがちで(属性で色々制御するのが楽しくなってしまう)、属性の取得はリフレクションの中でも特に遅いので高速化の恩恵は非常に大きいです。

 

 

以上の高速化コードによって、10~100倍以上の高速化を達成できました。
極振りと言いつつまだまだ最適化の余地はあると思いますので、興味のある方はサンプルコードを参考に更なる高速化を目指してみてください。

 

ちなみに、.NET のバージョンが上がるごとにリフレクションの処理速度は改善されています。
この記事の下書きを始めた頃は .NET 6.0 のプロジェクトを主に開発しており、式木(Expression Trees)を利用してリフレクションでのプロパティ値の取得(GetValue)、設定(SetValue)の処理を高速化していたのですが .NET 7.0 以降処理速度が大幅に改善され、今や式木の処理よりリフレクションの方が早くなってしまいました。
いずれ他のリフレクション関連の処理も処理速度を気にせず使えるようになるでしょう。

 

無駄になったコードの供養のため、ここにコードとベンチマーク結果を供えておきます。

using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

namespace Extensions;

/// <summary>
/// プロパティ関連拡張機能
/// </summary>
public static partial class PropertyExtension
{
    /// <summary>
    /// プロパティ値取得
    /// </summary>
    /// <typeparam name="T">オブジェクトの型</typeparam>
    /// <param name="info">プロパティ情報</param>
    /// <param name="obj">オブジェクト</param>
    /// <returns>プロパティ値</returns>
    public static object? EmGetValue<T>(this PropertyInfo info, T obj)
        where T : class
    {
        return ValueInterface<T, object>.GetValue(obj, info.Name);
    }

    /// <summary>
    /// プロパティ値設定
    /// </summary>
    /// <typeparam name="TObj">オブジェクトの型</typeparam>
    /// <typeparam name="TVal">設定する値の型</typeparam>
    /// <param name="info">プロパティ情報</param>
    /// <param name="obj">オブジェクト</param>
    /// <param name="value">設定する値</param>
    public static void EmSetValue<TObj, TVal>(this PropertyInfo info, TObj obj, TVal? value)
        where TObj : class
    {
        ValueInterface<TObj, TVal>.SetValue(obj, info.Name, value);
    }



    /// <summary>
    /// プロパティ値入出力クラス
    /// </summary>
    private static class ValueInterface<TObj, TVal>
        where TObj : class
    {
        /// <summary> プロパティ値取得デリゲートキャッシュ </summary>
        private static readonly ConcurrentDictionary<string, Func<TObj, TVal?>> Getters = new();

        /// <summary> プロパティ値設定デリゲートキャッシュ </summary>
        private static readonly ConcurrentDictionary<string, Action<TObj, TVal?>> Setters = new();


        /// <summary>
        /// プロパティ値取得
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="propName">プロパティ名</param>
        /// <returns>プロパティ値</returns>
        public static TVal? GetValue(TObj obj, string propName)
        {
            return ValueInterfaceHelper.GetValue(Getters, obj, propName);
        }

        /// <summary>
        /// プロパティ値設定
        /// </summary>
        /// <param name="obj">オブジェクト</param>
        /// <param name="propName">プロパティ名</param>
        /// <param name="val">設定する値</param>
        public static void SetValue(TObj obj, string propName, TVal? val)
        {
            ValueInterfaceHelper.SetValue(Setters, obj, propName, val);
        }
    }


    /// <summary>
    /// プロパティ値入出力ヘルパー
    /// </summary>
    private static class ValueInterfaceHelper
    {
        /// <summary>
        ///  プロパティ値取得
        /// </summary>
        /// <typeparam name="TObj">オブジェクトの型</typeparam>
        /// <typeparam name="TVal">設定する値の型</typeparam>
        /// <param name="cache">プロパティ値取得デリゲートキャッシュ</param>
        /// <param name="obj">オブジェクト</param>
        /// <param name="propName">プロパティ名</param>
        /// <returns>プロパティ値</returns>
        public static TVal? GetValue<TObj, TVal>(ConcurrentDictionary<string, Func<TObj, TVal?>> cache, TObj obj, string propName)
            where TObj : class
        {
            // プロパティ値取得デリゲート未生成の場合は生成&キャッシュ
            // 生成済みの場合はキャッシュから取得
            if (!cache.TryGetValue(propName, out var method))
            {
                method = CreateGetter<TObj, TVal>(propName);
                cache.AddOrUpdate(propName, method, (key, oldValue) => method);
            }

            // プロパティ値取得
            return method.Invoke(obj);
        }

        /// <summary>
        /// プロパティ値設定
        /// </summary>
        /// <typeparam name="TObj">オブジェクトの型</typeparam>
        /// <typeparam name="TVal">設定する値の型</typeparam>
        /// <param name="cache">プロパティ値設定デリゲートキャッシュ</param>
        /// <param name="obj">オブジェクト</param>
        /// <param name="propName">プロパティ名</param>
        /// <param name="val">設定する値</param>
        public static void SetValue<TObj, TVal>(ConcurrentDictionary<string, Action<TObj, TVal?>> cache, TObj obj, string propName, TVal? val)
            where TObj : class
        {
            // プロパティ値設定デリゲート未生成の場合は生成&キャッシュ
            // 生成済みの場合はキャッシュから取得
            if (!cache.TryGetValue(propName, out var method))
            {
                method = CreateSetter<TObj, TVal?>(propName);
                cache.AddOrUpdate(propName, method, (key, oldValue) => method);
            }

            // プロパティ値設定
            method.Invoke(obj, val);
        }

        /// <summary>
        /// プロパティ値取得デリゲート生成
        /// </summary>
        /// <typeparam name="TObj">オブジェクトの型</typeparam>
        /// <typeparam name="TVal">設定する値の型</typeparam>
        /// <param name="propName">プロパティ名</param>
        /// <returns>プロパティ値取得デリゲート</returns>
        private static Func<TObj, TVal?> CreateGetter<TObj, TVal>(string propName)
            where TObj : class
        {
            var target = Expression.Parameter(typeof(TObj), "target");
            var body = Expression.Convert(Expression.PropertyOrField(target, propName), typeof(TVal));
            var lambda = Expression.Lambda<Func<TObj, TVal?>>(body, target);
            return lambda.Compile();
        }

        /// <summary>
        /// プロパティ値設定デリゲート生成
        /// </summary>
        /// <typeparam name="TObj">オブジェクトの型</typeparam>
        /// <typeparam name="TVal">設定する値の型</typeparam>
        /// <param name="propName">プロパティ名</param>
        /// <returns>プロパティ値設定デリゲート</returns>
        private static Action<TObj, TVal?> CreateSetter<TObj, TVal>(string propName)
            where TObj : class
        {
            var target = Expression.Parameter(typeof(TObj), "target");
            var value = Expression.Parameter(typeof(TVal), "value");
            var left = Expression.PropertyOrField(target, propName);
            var right = Expression.Convert(value, left.Type);
            var lambda = Expression.Lambda<Action<TObj, TVal?>>(Expression.Assign(left, right), target, value);
            return lambda.Compile();
        }
    }
}
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
[MinColumn, MaxColumn]
public class BenchReflection
{
    private readonly ReflectionBenchmarkClass obj = new();
    private readonly PropertyInfo prop = typeof(ReflectionBenchmarkClass).GetProperty(nameof(ReflectionBenchmarkClass.String001))!;


    /// <summary> プロパティ値取得:直接 </summary>
    [Benchmark(Description = "プロパティ値取得:直接")]
    public string GetValue_Direct()
    {
        return obj.String001;
    }

    /// <summary> プロパティ値取得:キャッシュ無し </summary>
    [Benchmark(Description = "プロパティ値取得:リフレクション")]
    public object? GetValue_Reflection()
    {
        return prop.GetValue(obj);
    }

    /// <summary> プロパティ値取得:デリゲートキャッシュ </summary>
    [Benchmark(Description = "プロパティ値取得:式木")]
    public object? GetValue_ExpressionTrees()
    {
        return prop.EmGetValue(obj);
    }


    [Benchmark(Description = "プロパティ値セット:直接")]
    public void SetValue_Direct()
    {
        obj.String001 = "New Value";
    }

    [Benchmark(Description = "プロパティ値セット:リフレクション")]
    public void SetValue_Reflection()
    {
        prop.SetValue(obj, "New Value");
    }

    [Benchmark(Description = "プロパティ値セット:式木")]
    public void SetValue_ExpressionTrees()
    {
        prop.EmSetValue(obj, "New Value");
    }
}
Method Runtime Mean Error StdDev Median Min Max Gen0 Allocated
プロパティ値取得:直接 .NET 6.0 0.0000 ns 0.0000 ns 0.0000 ns 0.0000 ns 0.0000 ns 0.0000 ns
プロパティ値取得:リフレクション .NET 6.0 40.9719 ns 0.8478 ns 0.9424 ns 40.5003 ns 40.2336 ns 43.2940 ns
プロパティ値取得:式木 .NET 6.0 17.7688 ns 0.0839 ns 0.0700 ns 17.7661 ns 17.6531 ns 17.9032 ns 0.0029 24 B
プロパティ値セット:直接 .NET 6.0 1.0822 ns 0.0066 ns 0.0055 ns 1.0802 ns 1.0775 ns 1.0972 ns
プロパティ値セット:リフレクション .NET 6.0 68.2314 ns 1.1712 ns 1.0382 ns 67.6789 ns 67.5029 ns 70.5576 ns
プロパティ値セット:式木 .NET 6.0 19.6481 ns 0.0935 ns 0.0829 ns 19.6198 ns 19.5647 ns 19.8445 ns 0.0029 24 B
プロパティ値取得:直接 .NET 7.0 0.7501 ns 0.0215 ns 0.0201 ns 0.7405 ns 0.7331 ns 0.7986 ns
プロパティ値取得:リフレクション .NET 7.0 9.1580 ns 0.0147 ns 0.0131 ns 9.1527 ns 9.1408 ns 9.1892 ns
プロパティ値取得:式木 .NET 7.0 19.6968 ns 0.1322 ns 0.1104 ns 19.6685 ns 19.5481 ns 19.8609 ns 0.0029 24 B
プロパティ値セット:直接 .NET 7.0 1.8379 ns 0.0385 ns 0.0360 ns 1.8139 ns 1.8098 ns 1.9171 ns
プロパティ値セット:リフレクション .NET 7.0 26.2355 ns 0.2731 ns 0.2421 ns 26.1085 ns 26.0467 ns 26.7797 ns
プロパティ値セット:式木 .NET 7.0 21.5771 ns 0.2882 ns 0.2555 ns 21.4808 ns 21.3457 ns 22.1904 ns 0.0029 24 B
プロパティ値取得:直接 .NET 8.0 0.4863 ns 0.0026 ns 0.0022 ns 0.4860 ns 0.4841 ns 0.4921 ns
プロパティ値取得:リフレクション .NET 8.0 8.7046 ns 0.1765 ns 0.1564 ns 8.6828 ns 8.4107 ns 9.0196 ns
プロパティ値取得:式木 .NET 8.0 14.6060 ns 0.3182 ns 0.2977 ns 14.4509 ns 14.3176 ns 15.2991 ns 0.0029 24 B
プロパティ値セット:直接 .NET 8.0 0.0000 ns 0.0000 ns 0.0000 ns 0.0000 ns 0.0000 ns 0.0000 ns
プロパティ値セット:リフレクション .NET 8.0 14.7133 ns 0.3074 ns 0.3659 ns 14.5956 ns 14.3511 ns 15.6368 ns
プロパティ値セット:式木 .NET 8.0 15.7019 ns 0.2804 ns 0.2342 ns 15.7020 ns 15.3864 ns 16.1972 ns 0.0029 24 B
プロパティ値取得:直接 .NET 9.0 0.4451 ns 0.0082 ns 0.0064 ns 0.4443 ns 0.4398 ns 0.4633 ns
プロパティ値取得:リフレクション .NET 9.0 10.3213 ns 0.2047 ns 0.1815 ns 10.3179 ns 10.0850 ns 10.7073 ns
プロパティ値取得:式木 .NET 9.0 13.2344 ns 0.2097 ns 0.1859 ns 13.2425 ns 13.0316 ns 13.5815 ns 0.0029 24 B
プロパティ値セット:直接 .NET 9.0 0.0004 ns 0.0008 ns 0.0006 ns 0.0000 ns 0.0000 ns 0.0020 ns
プロパティ値セット:リフレクション .NET 9.0 13.5640 ns 0.0202 ns 0.0179 ns 13.5641 ns 13.5431 ns 13.5973 ns
プロパティ値セット:式木 .NET 9.0 14.5723 ns 0.2257 ns 0.2001 ns 14.6200 ns 14.1659 ns 14.8184 ns 0.0029 24 B

 

参考文献

【C#】リフレクションは遅い?処理時間実測と改善方法
https://docs.sakai-sc.co.jp/article/programing/csharp-reflection-performance.html

ちょっとだけStatic Type Cachingのパフォーマンス確認
https://cactuaroid.hatenablog.com/entry/2018/08/22/224454

【C#】TypeがKeyなDictionaryをStatic Type Cachingに置き換えて処理の高速化させる(実験付き)
https://www.hanachiru-blog.com/entry/2024/04/22/120000

C# のリフレクションの性能を測る。前から気になっていることを調べてみた
https://qiita.com/reimei2020/items/c815d16a02b1f8a88ba0

式木(Expression Trees) – C# によるプログラミング入門 | ++C++; // 未確認飛行 C
https://ufcpp.net/study/csharp/sp3_expression.html

Expression Tree プロパティマッパー
https://qiita.com/roundrobin/items/2ce465cbea8a51118987

【Unity】【C#】Expression TreeをReflectionの代わりに使う
https://light11.hatenadiary.com/entry/2019/05/08/235859

C#での動的なメソッド選択における定形高速化パターン
https://neue.cc/2014/01/27_446.html