【C# .NET10 Preview5】ローカルのデリゲートをスタックに置く最適化

# はじめに

[.NET10 Preview5](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0) が利用可能になりました。

今回は新機能のひとつ、[ローカルのデリゲートをスタックに置く最適化 / Escape Analysis for Delegates](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview5/runtime.md#escape-analysis-for-delegates) を見てみます。

## サンプルコード

<details>
<summary>テストコード</summary>

```cs
using System.Runtime.CompilerServices;

public class __EscapeAnalysisForDelegatesTest
{
    static void Performance(Performance p)
    {
        p.AddTest("Delegate", () =>
        {
            int sum = 0;
            for (int i = 0; i < 100000; i++)
            {
                int local = i;
                var func = (int x) => x + local;
                sum += func(i);
            }
        });

        p.AddTest("LocalMethod", () =>
        {
            int sum = 0;
            for (int i = 0; i < 100000; i++)
            {
                int local = i;
                [MethodImpl(MethodImplOptions.NoInlining)]
                int Func(int x) => x + local;
                sum += Func(i);
            }
        });

        p.AddTest("Inlining", () =>
        {
            int sum = 0;
            for (int i = 0; i < 100000; i++)
            {
                int local = i;
                [MethodImpl(MethodImplOptions.AggressiveInlining)]
                int Func(int x) => x + local;
                sum += Func(i);
            }
        });
    }
}
```

</details>

## パフォーマンス

```cs
p.AddTest("Delegate", () =>
{
    int sum = 0;
    for (int i = 0; i < 100000; i++)
    {
        int local = i;
        var func = (int x) => x + local;
        sum += func(i);
    }
});

p.AddTest("LocalMethod", () =>
{
    int sum = 0;
    for (int i = 0; i < 100000; i++)
    {
        int local = i;
        [MethodImpl(MethodImplOptions.NoInlining)]
        int Func(int x) => x + local;
        sum += Func(i);
    }
});

p.AddTest("Inlining", () =>
{
    int sum = 0;
    for (int i = 0; i < 100000; i++)
    {
        int local = i;
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        int Func(int x) => x + local;
        sum += Func(i);
    }
});
```

|Test|Score|%|GC0|
|---|---:|---:|---:|
|**.NET10 x86**||||
|Delegate|*144*|100.0%|**121**|
|LocalMethod|636|441.7%|0|
|Inlining|4,454|3,093.1%|0|
|*.NET9 x86*||||
|Delegate|**159**|100.0%|*133*|
|LocalMethod|632|397.5%|0|
|Inlining|4,430|2,786.2%|0|

実行環境: Windows11 x64 .NET Runtime 10.0.0  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

- .NET10 は .NET9 と比較して GC 回数が減っています
- .NET10 は .NET9 と比較してパフォーマンスが**少し悪くなっています**。何回か計測しましたがこの傾向です
- ~~ローカル関数使うとよさそう~~

## パフォーマンス結果から考えてみる

GC 回数が減ったのは今回の最適化によってデリゲートオブジェクトがスタックに置かれるようになったと思われます。GC が 0 になっていませんが、これはローカル変数キャプチャによるクロージャオブジェクトが作られるからですね。

参考：ローカル関数と匿名関数#ローカル変数の捕獲 <https://ufcpp.net/study/csharp/functional/fun_localfunctions#capture-local>

一方でパフォーマンスが微妙に下がっている理由は・・・よくわかりません。まだ preview 版ですし、ランタイム最適化が複雑で思いのほか解析コストがあるのかもしれません。

ローカル関数の方はいろいろ最適化がかかってパフォーマンスが出ます。インライン化もされます。

## おわりに

自分の印象だと、今回の最適化は .NET10 で力を入れている `クラスオブジェクトの Stack 配置` 最適化の一部お披露目といった感じです（大掛かりな最適化の、できたところから順に公開してフィードバックを取り入れる）。ランタイムがかなり仕事をするので、様々な環境でバグなくパフォーマンスも上げるのはかなり根気のいる作業ですね・・・

[.NET10 Preview5](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0)

関連  
[【C#】.NET10 Preview1 ｷﾀ━━(ﾟ∀ﾟ)━━!!](https://qiita.com/abetakahiro123/items/2153df9795cd0f12da47)  
[【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証](https://qiita.com/abetakahiro123/items/9113ee6347580c5d0e39)  
[【C# .NET10 Preview2】参照型がスタックに置かれる最適化](https://qiita.com/abetakahiro123/items/cedd77ba25de1b8db8d1)  
[【C# .NET10 Preview3】null 条件付き代入](https://qiita.com/abetakahiro123/items/db05ae0cab986183cfab)  
[【C# .NET10 Preview3】参照型の小さな配列のスタック割り当て](https://qiita.com/abetakahiro123/items/339412e890dfe6274941)  
[【C# .NET10 Preview3】拡張メソッドの機能追加](https://qiita.com/abetakahiro123/items/2599717fee4998774d5d)  
[【C# .NET10 Preview4】try-finally をインライン化する最適化の追加](https://qiita.com/abetakahiro123/items/5b8064c5d546f32f2c76)  
[【C# .NET10 Preview4】非同期 ZIP アーカイブ](https://qiita.com/abetakahiro123/items/eeff1acf5cde4549e104)