[C#] リフレクションでなるべく高速にクラスメンバを取得

リフレクションの高速化は、既に先人が研究し尽くしてる感のあるテクニックですが、計測結果などがまとまったものが見つからなかったので、改めて自分で速度を測ってみました。

```cs:テスト用コード
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Linq.Expressions;
using System.Reflection;

public class ValueClass
{
    public int ValueProperty { get; set; } = 10;
    public int ValueField = 20;
}

public class Accessor
{
    public static Func<TClass, TValue> CreateGetterDelegate<TClass, TValue>(string propertyName)
        => (Func<TClass, TValue>)Delegate.CreateDelegate(
            typeof(Func<TClass, TValue>),
            typeof(TClass).GetProperty(propertyName).GetMethod
            );

    public static Func<TClass, TValue> CreateGetterExpression<TClass, TValue>(string name)
    {
        var instance = Expression.Parameter(typeof(TClass));
        var member = Expression.PropertyOrField(instance, name);
        return (Func<TClass, TValue>)Expression.Lambda(member, instance).Compile();
    }

    public static Func<object, object> CreateGetterExpression(Type typeClass, string name)
    {
        var oinstance = Expression.Parameter(typeof(object));
        var instance = Expression.Convert(oinstance, typeClass);
        var member = Expression.PropertyOrField(instance, name);
        var omember = Expression.Convert(member, typeof(object));
        return (Func<object, object>)Expression.Lambda(omember, oinstance).Compile();
    }
}

[ShortRunJob]
[MemoryDiagnoser(false)]
public class ReflectionBench
{
    private static PropertyInfo _CacheGetPropertyInfo
        = typeof(ValueClass).GetProperty(nameof(ValueClass.ValueProperty));

    private static Func<ValueClass, int> _CacheGetPropertyDelegate
        = Accessor.CreateGetterDelegate<ValueClass, int>(nameof(ValueClass.ValueProperty));

    private static Func<ValueClass, int> _CachePropertyExpression
        = Accessor.CreateGetterExpression<ValueClass, int>(nameof(ValueClass.ValueProperty));

    private static Func<object, object> _CacheGetPropertyExpressionObj
        = Accessor.CreateGetterExpression(typeof(ValueClass), nameof(ValueClass.ValueProperty));

    private static FieldInfo _CacheGetFieldInfo
        = typeof(ValueClass).GetField(nameof(ValueClass.ValueField));

    private static Func<ValueClass, int> _CacheGetFieldExpression
        = Accessor.CreateGetterExpression<ValueClass, int>(nameof(ValueClass.ValueField));

    private static Func<object, object> _CacheGetFieldExpressionObj
        = Accessor.CreateGetterExpression(typeof(ValueClass), nameof(ValueClass.ValueField));

    private static ValueClass _ValueClass = new ValueClass();

    // 直接プロパティ取得
    [Benchmark]
    public int GetProperty()
        => _ValueClass.ValueProperty;

    // PropertyInfoでプロパティ取得
    [Benchmark]
    public int GetPropertyReflection()
        => (int)_CacheGetPropertyInfo.GetValue(_ValueClass);

    // PropertyInfoからデリゲートを作成してプロパティ取得
    [Benchmark]
    public int GetPropertyDelegate()
        => _CacheGetPropertyDelegate(_ValueClass);

    // Expressionでプロパティ取得
    [Benchmark]
    public int GetPropertyExpression()
        => _CachePropertyExpression(_ValueClass);

    // Expressionでプロパティ取得（object型経由）
    [Benchmark]
    public int GetPropertyExpressionObj()
        => (int)_CacheGetPropertyExpressionObj(_ValueClass);

    // 直接フィールド取得
    [Benchmark]
    public int GetField()
        => _ValueClass.ValueField;

    // FieldInfoでフィールド取得
    [Benchmark]
    public int GetFieldReflection()
        => (int)_CacheGetFieldInfo.GetValue(_ValueClass);

    // Expressionでフィールド取得
    [Benchmark]
    public int GetFieldExpression()
        => _CacheGetFieldExpression(_ValueClass);

    // Expressionでフィールド取得（object型経由）
    [Benchmark]
    public int GetFieldExpressionObj()
        => (int)_CacheGetFieldExpressionObj(_ValueClass);
}

public class Program
{
    public static void Main()
    {
#if DEBUG
        var cls = new ReflectionBench();
        Console.WriteLine(cls.GetProperty());
        Console.WriteLine(cls.GetPropertyReflection());
        Console.WriteLine(cls.GetPropertyDelegate());
        Console.WriteLine(cls.GetPropertyExpression());
        Console.WriteLine(cls.GetPropertyExpressionObj());
        Console.WriteLine(cls.GetField());
        Console.WriteLine(cls.GetFieldReflection());
        Console.WriteLine(cls.GetFieldExpression());
        Console.WriteLine(cls.GetFieldExpressionObj());
        Console.ReadLine();
#else
        BenchmarkRunner.Run<ReflectionBench>();
#endif
    }
}
```

### .NET Framework 4.8.1 (x64)
| Method                     | Mean       | Error      | StdDev    | Allocated |
|--------------------------- |-----------:|-----------:|----------:|----------:|
| GetProperty                |  0.0352 ns |  0.2141 ns | 0.0117 ns |         - |
| GetPropertyReflection      | 88.2162 ns | 38.6461 ns | 2.1183 ns |      24 B |
| GetPropertyDelegate        |  2.2311 ns |  0.0674 ns | 0.0037 ns |         - |
| GetPropertyExpression      |  4.6539 ns |  1.5632 ns | 0.0857 ns |         - |
| GetPropertyExpressionObj   |  7.3046 ns |  2.5343 ns | 0.1389 ns |      24 B |
| GetField                   |  0.0338 ns |  0.1297 ns | 0.0071 ns |         - |
| GetFieldReflection         | 45.1978 ns | 34.1884 ns | 1.8740 ns |      24 B |
| GetFieldExpression         |  0.2263 ns |  0.0259 ns | 0.0014 ns |         - |
| GetFieldExpressionObj      |  3.0927 ns |  0.9122 ns | 0.0500 ns |      24 B |

.NET Frameworkでは、プロパティの取得はExpressionよりPropertyInfoから作成したデリゲートの方が速いようです。フィールドの取得は、FieldInfoからはCreateDelegate出来ないので、Expressionはかなり効果があります。

### .NET 8 (x64)
| Method                   | Mean       | Error     | StdDev    | Allocated |
|------------------------- |-----------:|----------:|----------:|----------:|
| GetProperty              |  0.2103 ns | 0.1799 ns | 0.0099 ns |         - |
| GetPropertyReflection    |  9.7578 ns | 2.1925 ns | 0.1202 ns |      24 B |
| GetPropertyDelegate      |  0.6400 ns | 0.0072 ns | 0.0004 ns |         - |
| GetPropertyExpression    |  0.2227 ns | 0.1074 ns | 0.0059 ns |         - |
| GetPropertyExpressionObj |  3.8620 ns | 1.1930 ns | 0.0654 ns |      24 B |
| GetField                 |  0.2165 ns | 0.0452 ns | 0.0025 ns |         - |
| GetFieldReflection       | 28.9341 ns | 4.3855 ns | 0.2404 ns |      24 B |
| GetFieldExpression       |  0.2135 ns | 0.1056 ns | 0.0058 ns |         - |
| GetFieldExpressionObj    |  3.7763 ns | 0.1409 ns | 0.0077 ns |      24 B |

.NET8は、Expressionでの取得がプロパティ・フィールド共に直接アクセスと遜色ないレベルで速いです。また、普通にリフレクションした場合の速度も全体的に改善されているようです。（とはいえ、相変わらず直接アクセスと比較するとかなり遅いですが）

## 参考記事
[リフレクションは遅いから情報をキャッシュするとかのお話](https://yamaokuno-usausa.hatenablog.com/entry/20090821/1250813986)
[Expression Treeのこね方・入門編 - 動的にデリゲートを生成してリフレクションを高速化](https://neue.cc/2011/04/20_317.html)
[Faster than Reflection: Delegates - Part 1](https://www.codeproject.com/Articles/1118828/Faster-than-Reflection-Delegates-Part)