【C#】
リフレクションは遅い?処理時間実測と改善方法

記事イメージ

C#におけるリフレクション機能(System.Reflection)は、メタプログラミングや抽象化に重要な機能です。

一方で処理速度は遅めであることが知られていますが、具体的にどの機能がどれくらい遅いのか?は知っておきたい要素です。(リフレクションと一口に言っても色々機能があります。)

そこで今回は、リフレクションに含まれる各機能の実際の処理速度の実測と、プロパティへのアクセス高速化手法について紹介します。

各機能実測。属性取得は特に遅い。値の取得・更新も、通常のプロパティ更新の10倍オーダー。

リフレクション及び周辺機能について、1,000万回のループで実行し、System.Diagnostics.Stopwatchクラスで測定します。

測定に使用したコードはこの記事の末尾(ページ内リンク)に掲載します。

表:1,000万回ループで実行してStopwatchクラスで測定(.NET6.0、C#10.0)※msec=ミリ秒
処理 1,000万回実行時間
(実測)
実行コード
基準値
(プロパティに値代入)
70msec testInstance.TestProp3 = i;
型の取得(typeof) 25msec typeof(TestClass);
プロパティ属性の取得 5,476msec propertyInfo.GetCustomAttribute<TestAttribute>();
クラス属性の取得 8,259msec testType.GetCustomAttribute<TestAttribute>();
プロパティの列挙 391msec testType.GetProperties();
プロパティ値の取得 459msec propertyInfo.GetValue(testInstance);
プロパティ値の更新 739msec propertyInfo.SetValue(testInstance, "fuga");
プロパティ値の取得
(デリゲート保持)
65msec accesser.GetValue(testInstance);
プロパティ値の更新
(デリゲート保持)
100msec accesser.SetValue(testInstance, "fuga");

※「デリゲート保持」と記載した項目は、あらかじめgetter/setterをデリゲートとして取得して使いまわす手法です。次のセクションでコードを含め解説します。

表のとおり、属性取得は特に負荷がかかります。ループで使用する際は、属性取得は最低限となるようにあらかじめ変数に格納しておくなど実装方法の工夫が求められます。

また、その他の処理も基準値(通常の代入処理)に比べて5~10倍の時間がかかっています。要素数の多いリストなど処理する場合、相応の影響が想定されますので、次のセクションで紹介する手法をご検討ください。

高速化のための、デリゲートを保持してのプロパティ値の取得・更新

上の表のとおり、プロパティ値の取得・更新は、デリゲートを含んだアクセサーをあらかじめ作成しておくことで、処理時間を5分の1~10分の1程度に削減できます。

GetAccessorの処理(デリゲートの生成)の箇所は処理に時間がかかります。一旦取得したもの(↓のコードではaccessor変数)を保持して使いまわすようにしましょう。

尚、下記のdelegate取得処理の実装箇所については、GitHubのリポジトリ(外部リンク)にも格納してあります。

C#
using System.Reflection;

//PropertyInfoからgetter/setterを含むaccessorを取得
var testInstance = new TestClass();
var propertyInfo = typeof(TestClass).GetProperties()[0];
var accessor = propertyInfo.GetAccessor();

//プロパティにアクセス
//取得
var val = accessor.GetValue(testInstance);
//更新
accessor.SetValue(testInstance, "fuga");


//以下、delegate取得処理の実装

public interface IAccessor {
    object? GetValue(object target);
    void SetValue(object target, object? value);
}

internal sealed class Accessor<TTarget, TProperty> : IAccessor {
    private readonly Func<TTarget, TProperty>? Getter;
    private readonly Action<TTarget, TProperty>? Setter;

    public Accessor(Func<TTarget, TProperty>? getter, Action<TTarget, TProperty>? setter) {
        this.Getter = getter;
        this.Setter = setter;
    }

    public object? GetValue(object target) {
        if (this.Getter == null) return null;
        return this.Getter((TTarget)target);
    }

    public void SetValue(object target, object? value) {
        if (this.Setter != null && value != null) {
            this.Setter((TTarget)target, _changeType(value));
        }
    }

    private TProperty _changeType(object value) {
        var typeOfProperty = typeof(TProperty);

        if (typeOfProperty.IsGenericType && typeOfProperty.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) {
            typeOfProperty = Nullable.GetUnderlyingType(typeOfProperty);
        }
        return (TProperty)Convert.ChangeType(value, typeOfProperty!);
    }
}

public static class DynamicPropertyAccessPropertyExtension {

    public static IAccessor GetAccessor(this PropertyInfo property) {

        Type getterDelegateType = typeof(Func<,>).MakeGenericType(property.DeclaringType!, property.PropertyType);
        var getMethod = property.GetGetMethod();
        Delegate? getter = getMethod switch {
            null => null,
            _ => Delegate.CreateDelegate(getterDelegateType, getMethod)
        };

        Type setterDelegateType = typeof(Action<,>).MakeGenericType(property.DeclaringType!, property.PropertyType);
        var setMethod = property.GetSetMethod();
        Delegate? setter = setMethod switch {
            null => null,
            _ => Delegate.CreateDelegate(setterDelegateType, setMethod)
        };

        Type accessorType = typeof(Accessor<,>).MakeGenericType(property.DeclaringType!, property.PropertyType);
        return (IAccessor)Activator.CreateInstance(accessorType, getter, setter)!;
    }
}

具体的な実装方法

accessorをあらかじめ変数に保持して使いまわすことで、リフレクションベースでのGetValue/SetValueに比べて高速化の効果を発揮します。

↓のコードのように、同じクラスのインスタンスを多量に生成する際などに有効です。

C#
using System;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
//https://github.com/Sakai3578/DelegeteToPropertyAccess
using DelegeteToPropertyAccess; 

//TestClassのプロパティと、対応するアクセサーを、インスタンス生成ループ処理の前に列挙しておく
var properties = typeof(TestClass).GetProperties();
var accessors = properties.Select(x => x.GetAccessor()).ToList(); //ここがポイント!プロパティに対応するアクセサーを事前生成

//String型の2次元リストstring2dListからTestClassのリストを生成ループ
var testClassInstanses = new List<TestClass>();
for (int i = 0; i < string2dList.Count; i++) {
    var t = new TestClass();
    
    for (int j = 0; j < accessors.Length; j++) {
        accessors[j].SetValue(t, string2dList[i][j]);
    }
    
    testClassInstanses.Add(t);
}

取得で46回未満、更新で28回未満であれば、リフレクションの方が早い(実測結果)

上述の通り、GetAccessorの処理はそれなりの時間を要します。

具体的には、次のコードでGetAccessorを1,000万回実行したところ、処理時間は17,869msecという結果でした。

C#
for (int i = 0; i < 10000000; i++) {
    propertyInfo.GetAccessor();
}

リフレクションによるプロパティ値の取得・更新に比べても20~30倍の時間がかかっているので、ループ数が少ない場合はリフレクションの方が高速ということになります。

今回得られている各数値に基づいて具体的に線形計算をしてみると、プロパティ値の取得で46回以上、更新で28回以上のループでAccessor利用の方が高速になるという結果でした(下グラフ参照)。

プロパティ値の取得の回数と経過時間の比較(PropertyInfo.GetValue vs Accessor)。46回以上のループでAccessor利用の方が高速になった。
プロパティ値の取得の回数と経過時間の比較(PropertyInfo.GetValue vs Accessor)

グラフの計算式:プロパティ値【取得】

 PropertyInfo.GetValue:(459×試行回数)÷10,000,000

 Accessor:(17,869+65×試行回数)÷10,000,000

プロパティ値の更新の回数と経過時間の比較(PropertyInfo.GetValue vs Accessor)。28回以上のループでAccessor利用の方が高速になった。
プロパティ値の更新の回数と経過時間の比較(PropertyInfo.GetValue vs Accessor)

グラフの計算式:プロパティ値【更新】

 PropertyInfo.GetValue:(739×試行回数)÷10,000,000

 Accessor:(17,869+100×試行回数)÷10,000,000

※結果は環境によって異なります。

(参考)処理速度の検証に使用したコード

以下のコードを使用して作表しました。尚、Stopwatchクラスはコード内で高精度の時間測定が出来る組み込みクラスです。

C#
using System.Diagnostics;
using System.Reflection;
//https://github.com/Sakai3578/DelegeteToPropertyAccess
using DelegeteToPropertyAccess; 

var TRIAL_NUMBER = 10000000;

//基準値(プロパティに値に代入する処理)
var stopwatch = new Stopwatch();
var testInstance = new TestClass();
stopwatch.Start();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    testInstance.TestProp3 = i;
}
stopwatch.Stop();
Console.WriteLine("基準値(プロパティに値に代入する処理):" + stopwatch.ElapsedMilliseconds);

//型の取得(typeof)
stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    var a = typeof(TestClass);
}
stopwatch.Stop();
Console.WriteLine("型の取得(typeof):" + stopwatch.ElapsedMilliseconds);

//プロパティ属性の取得
stopwatch = new Stopwatch();
stopwatch.Start();
var propertyInfo = typeof(TestClass).GetProperties()[0];
for (int i = 0; i < TRIAL_NUMBER; i++) {
    var attr = propertyInfo.GetCustomAttribute<TestAttribute>();
}
stopwatch.Stop();
Console.WriteLine("プロパティ属性の取得:" + stopwatch.ElapsedMilliseconds);

//クラス属性の取得
stopwatch = new Stopwatch();
stopwatch.Start();
var testType = typeof(TestClass);
for (int i = 0; i < TRIAL_NUMBER; i++) {
    var attr = testType.GetCustomAttribute<TestAttribute>();
}
stopwatch.Stop();
Console.WriteLine("クラス属性の取得:" + stopwatch.ElapsedMilliseconds);

//プロパティの列挙
stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    var properties = testType.GetProperties();
}
stopwatch.Stop();
Console.WriteLine("プロパティの列挙:" + stopwatch.ElapsedMilliseconds);

//値の取得
testInstance = new TestClass() { TestProp1 = "hoge" };
propertyInfo = typeof(TestClass).GetProperties()[0];
stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    var val = propertyInfo.GetValue(testInstance);
}
stopwatch.Stop();
Console.WriteLine("値の取得:" + stopwatch.ElapsedMilliseconds);

//値の更新
testInstance = new TestClass() { TestProp1 = "hoge" };
propertyInfo = typeof(TestClass).GetProperties()[0];
stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    propertyInfo.SetValue(testInstance, "fuga");
}
stopwatch.Stop();
Console.WriteLine("値の更新:" + stopwatch.ElapsedMilliseconds);


//値の取得(アクセサー経由)
testInstance = new TestClass() { TestProp1 = "hoge" };
propertyInfo = typeof(TestClass).GetProperties()[0];
stopwatch = new Stopwatch();
stopwatch.Start();
var accesser = propertyInfo.GetAccessor();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    accesser!.GetValue(testInstance);
}
stopwatch.Stop();
Console.WriteLine("値の取得(デリゲート経由):" + stopwatch.ElapsedMilliseconds);

//値の更新(アクセサー経由)
testInstance = new TestClass() { TestProp1 = "hoge" };
propertyInfo = typeof(TestClass).GetProperties()[0];
stopwatch = new Stopwatch();
stopwatch.Start();
accesser = propertyInfo.GetAccessor();
for (int i = 0; i < TRIAL_NUMBER; i++) {
    accesser!.SetValue(testInstance, "fuga");
}
stopwatch.Stop();
Console.WriteLine("値の更新(デリゲート経由):" + stopwatch.ElapsedMilliseconds);


class TestAttribute : Attribute {
}

[Test]
class TestClass {
    [Test]
    public string? TestProp1 { get; set; }
    [Test]
    public string? TestProp2 { get; set; }
    [Test]
    public int TestProp3 { get; set; }
}

C#の処理高速化テクニックについて、兄弟記事を公開しています

本記事ではリフレクションの速度と改善にターゲットして記載しましたが、その他の処理高速化のTipsを別途公開しています。是非あわせてご参照ください。

関連記事:【C#】処理速度のケース別実測検証、遅くなる記法と解決方法

記事筆者へのお問い合わせ、仕事のご依頼

当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。

この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。

皆様のご投稿をお待ちしております。

記事筆者へ問い合わせする

※ご相談は無料でお受けいたします。
IT活用経営を実現する - 堺財経電算合同会社