C# のリフレクションの性能を測る。前から気になっていることを調べてみた

# はじめに
- 個人的にはやめろとか思うのですが、よくバックエンドのロジックで、こんな風なのを毎回実行しているコードを見かけます。
```csharp
model.GetType().GetProperty("Name")?.SetValue(model, "aaa");
```
以下のことを思います。
 - GetProperty は毎回取得する必要がない、
 - リフレクションだから遅い。
 - 素直にName="aaa" として静的コード実行したほうが早い。
 - せめて式木でキャッシュしてください。
 - 1個WebApiとかで、１個の要求として考えたらたらたいしたことないけどサーバー全体して考えて、かつ共通である程度のロジックから呼び出せれているなら早く終わるコードの方がよい。

.NET5 とか.NET 6とかでリフレクション速度改善されているなんて声も聴きベンチマークをしてみました。
※2023/11/12 追記各Runtimeごとの性能を図ってみる。

# ベンチマークコード
ベンチマーク.NET を使い以下のようなコードを書きました。

``` csharp
[SimpleJob(RuntimeMoniker.Net481)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
[SimpleJob(RuntimeMoniker.Net50)]
[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
[AsciiDocExporter]
[MarkdownExporterAttribute.Default]
[HtmlExporter]
public class ReflectionTest
{
    private static readonly PropertyInfo propertyInfoId;
    private static readonly PropertyInfo propertyInfoName;
    private static readonly Action<object, object> treeId;
    private static readonly Action<object, object> treeName;

    static ReflectionTest()
    {
        propertyInfoId = typeof(TestModel).GetProperty("Id")!;
        propertyInfoName = typeof(TestModel).GetProperty("Name")!;
        treeId = SetDelegate(typeof(TestModel), "Id");
        treeName = SetDelegate(typeof(TestModel), "Name");
    }
    static Action<object, object> SetDelegate(Type type, string memberName)
    {
        var target = Expression.Parameter(typeof(object), "target");
        var value = Expression.Parameter(typeof(object), "value");

        var left =
            Expression.PropertyOrField(
                Expression.Convert(target, type), memberName);

        var right = Expression.Convert(value, left.Type);

        var lambda = Expression.Lambda<Action<object, object>>(
            Expression.Assign(left, right),
            target, value);

        return lambda.Compile();
    }


    [Benchmark]
    public void NonReflection()
    {
        var model = new TestModel();
        model.Id = 1;
        model.Name = "aaa";
    }

    [Benchmark]
    public void Reflection()
    {
        var model = new TestModel();
        model.GetType().GetProperty("Id")?.SetValue(model, 1);
        model.GetType().GetProperty("Name")?.SetValue(model, "aaa");
    }
    [Benchmark]
    public void CacheReflection()
    {
        var model = new TestModel();
        propertyInfoId.SetValue(model, 1);
        propertyInfoName.SetValue(model, "aaa");
    }
    [Benchmark]
    public void ExpressionTree()
    {
        var model = new TestModel();
        treeId(model, 1);
        treeName(model, "aaa");

    }
}
```
- projectファイルに以下を追加
``` xml
 <TargetFrameworks>net7.0;net6.0;net48;netcoreapp3.1;net5.0;net8.0</TargetFrameworks>
```
# 実行結果

BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.2428/23H2/2023Update/SunValley3)
13th Gen Intel Core i7-13700H, 1 CPU, 20 logical and 14 physical cores
.NET SDK 8.0.100-rc.2.23502.2
  [Host]               : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
  .NET 5.0             : .NET 5.0.17 (5.0.1722.21314), X64 RyuJIT AVX2
  .NET 6.0             : .NET 6.0.24 (6.0.2423.51814), X64 RyuJIT AVX2
  .NET 7.0             : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2
  .NET 8.0             : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2
  .NET Core 3.1        : .NET Core 3.1.32 (CoreCLR 4.700.22.55902, CoreFX 4.700.22.56512), X64 RyuJIT AVX2
  .NET Framework 4.8.1 : .NET Framework 4.8.1 (4.8.9181.0), X64 RyuJIT VectorSize=256

| Method          | Runtime              | Mean       | Error     | StdDev    | Ratio | Gen0   | Allocated |
|---------------- |--------------------- |-----------:|----------:|----------:|------:|-------:|----------:|
| NonReflection   | .NET 5.0             |   3.834 ns | 0.0369 ns | 0.0345 ns |  1.00 | 0.0025 |      32 B |
| Reflection      | .NET 5.0             | 176.898 ns | 0.8622 ns | 0.8065 ns | 46.14 | 0.0145 |     184 B |
| CacheReflection | .NET 5.0             | 129.256 ns | 1.0773 ns | 1.0077 ns | 33.71 | 0.0145 |     184 B |
| ExpressionTree  | .NET 5.0             |   6.791 ns | 0.0521 ns | 0.0462 ns |  1.77 | 0.0045 |      56 B |
|                 |                      |            |           |           |       |        |           |
| NonReflection   | .NET 6.0             |   4.052 ns | 0.0566 ns | 0.0502 ns |  1.00 | 0.0025 |      32 B |
| Reflection      | .NET 6.0             | 116.984 ns | 1.1387 ns | 0.8890 ns | 28.78 | 0.0043 |      56 B |
| CacheReflection | .NET 6.0             |  88.294 ns | 0.3755 ns | 0.3513 ns | 21.79 | 0.0044 |      56 B |
| ExpressionTree  | .NET 6.0             |   7.408 ns | 0.0394 ns | 0.0368 ns |  1.83 | 0.0045 |      56 B |
|                 |                      |            |           |           |       |        |           |
| NonReflection   | .NET 7.0             |   3.262 ns | 0.0866 ns | 0.1669 ns |  1.00 | 0.0025 |      32 B |
| Reflection      | .NET 7.0             |  64.454 ns | 1.2831 ns | 1.8401 ns | 19.84 | 0.0044 |      56 B |
| CacheReflection | .NET 7.0             |  35.141 ns | 0.7297 ns | 1.8308 ns | 10.81 | 0.0044 |      56 B |
| ExpressionTree  | .NET 7.0             |   6.885 ns | 0.1626 ns | 0.3637 ns |  2.11 | 0.0045 |      56 B |
|                 |                      |            |           |           |       |        |           |
| NonReflection   | .NET 8.0             |   2.135 ns | 0.0608 ns | 0.0624 ns |  1.00 | 0.0025 |      32 B |
| Reflection      | .NET 8.0             |  43.243 ns | 0.2352 ns | 0.2085 ns | 20.21 | 0.0044 |      56 B |
| CacheReflection | .NET 8.0             |  21.091 ns | 0.1027 ns | 0.0910 ns |  9.86 | 0.0044 |      56 B |
| ExpressionTree  | .NET 8.0             |   5.552 ns | 0.0300 ns | 0.0266 ns |  2.59 | 0.0045 |      56 B |
|                 |                      |            |           |           |       |        |           |
| NonReflection   | .NET Core 3.1        |   3.597 ns | 0.0442 ns | 0.0413 ns |  1.00 | 0.0025 |      32 B |
| Reflection      | .NET Core 3.1        | 193.061 ns | 0.9542 ns | 0.8925 ns | 53.68 | 0.0145 |     184 B |
| CacheReflection | .NET Core 3.1        | 148.658 ns | 0.8488 ns | 0.6627 ns | 41.32 | 0.0145 |     184 B |
| ExpressionTree  | .NET Core 3.1        |   7.168 ns | 0.1127 ns | 0.1652 ns |  2.01 | 0.0045 |      56 B |
|                 |                      |            |           |           |       |        |           |
| NonReflection   | .NET Framework 4.8.1 |   3.574 ns | 0.0365 ns | 0.0342 ns |  1.00 | 0.0051 |      32 B |
| Reflection      | .NET Framework 4.8.1 | 264.374 ns | 0.9149 ns | 0.8110 ns | 74.04 | 0.0291 |     185 B |
| CacheReflection | .NET Framework 4.8.1 | 192.431 ns | 0.8518 ns | 0.6651 ns | 53.93 | 0.0293 |     185 B |
| ExpressionTree  | .NET Framework 4.8.1 |  12.734 ns | 0.0410 ns | 0.0384 ns |  3.56 | 0.0089 |      56 B |

- .NET 7.0 なった時にだいぶリフレクションの性能が改善されているのがわかった。個人的なイメージはxxx ns→x ns くらいの印象だったので、とはいえ、19倍とか20倍の性能差ならリフレクション使わない方がいいかなと、propertyinfo キャッシュするだけでも、それなりには改善されることはわかった。.NETのバージョンが上がるたびに早くはなっていっている。
- ただ今の時代リフレクションを使わずに問題のようなコードはソースジェネレーターで静的生成する方がよいなと思いました。Entityにある共通的な項目とかを設定するだけなら。