【.NET10 Preview4】try-finally をインライン化する最適化の追加

# はじめに

[.NET10 Preview4](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0) が利用可能になりました。  
今回はランタイムの機能強化である [インライン化の改善/Inlining Improvements](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview4/runtime.md#inlining-improvements) を確認してみます。

※ 機械翻訳です

> try-finally ブロックを持つメソッドのインライン化が可能になりました。  
> この変更によりインライン候補の数が増えたため、インライナーがリソースを使い果たす頻度が高くなりました。ランタイムはインライナーの時間制約を2倍にすることでこれに対処しました。
> JIT は、あるコールサイトがインライニングに不利であると判断した場合、そのメソッドに NoInlining のマークを付け、将来インライニングを試みる際にそのメソッドを考慮しなくて済むようにし、コンパイル時間を短縮する。しかし、多くのインライン・ヒューリスティックはプロファイル・データに敏感です。たとえば、プロファイルデータがない場合、JIT はメソッドが大きすぎてインライン化する価値がないと判断するかもしれません。一方、呼び出し元が十分にホットな場合、JIT はサイズ制限を緩和して呼び出しをインライン化するかもしれません。変更により、JIT が NoInlining で採算の合わないインラインにフラグを立てなくなり、プロファイルデータによる呼び出しサイトの過少評価を回避できるようになりました。


## テストコード

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

```cs
using System.Runtime.CompilerServices;

public class __InliningImprovementsTest
{
    static void InliningImprovements(Performance p)
    {
        var tmp = 0;
        p.AddTest("NoInlining", () =>
        {
            [MethodImpl(MethodImplOptions.NoInlining)]
            int GetNumber(int number)
            {
                ++tmp;
                return number * 2;
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });

        p.AddTest("Inlining", () =>
        {
            int GetNumber(int number)
            {
                ++tmp;
                return number * 2;
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });

        p.AddTest("Try-Finally", () =>
        {
            int GetNumber(int number)
            {
                try
                {
                    return number * 2;
                }
                finally
                {
                    ++tmp;
                }
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });

        p.AddTest("Try-Catch", () =>
        {
            int GetNumber(int number)
            {
                try
                {
                    return number * 2;
                }
                catch
                {
                    return 0;
                }
                finally
                {
                    ++tmp;
                }
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });
    }

    static void Inlining_Using(Performance p)
    {
        p.AddTest("NoInlining", () =>
        {
            [MethodImpl(MethodImplOptions.NoInlining)]
            int GetNumber(int number)
            {
                using var resource = new MyResource(number);
                return resource.Number * 2;
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });

        p.AddTest("Inlining", () =>
        {
            int GetNumber(int number)
            {
                using var resource = new MyResource(number);
                return resource.Number * 2;
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });
    }

    static void Inlining_Lock(Performance p)
    {
        Lock handle = new();
        p.AddTest("NoInlining", () =>
        {
            [MethodImpl(MethodImplOptions.NoInlining)]
            int GetNumber(int number)
            {
                lock (handle)
                {
                    return number * 2;
                }
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });

        p.AddTest("Inlining", () =>
        {
            int GetNumber(int number)
            {
                lock (handle)
                {
                    return number * 2;
                }
            }

            var sum = 0;
            for (var i = 0; i < 1000000; i++)
            {
                sum += GetNumber(i);
            }
        });
    }

    void SugarSyntax()
    {
        // using は try-finally の糖衣構文
        using var source1 = new MyResource();

        var source2 = new MyResource();
        try
        {
            // 省略
        }
        finally
        {
            source2.Dispose();
        }

        var handle1 = new Lock();
        // lock は try-finally の糖衣構文
        lock (handle1)
        {
            // 省略
        }

        var handle2 = new object();
        try
        {
            System.Threading.Monitor.Enter(handle2);

            // 省略
        }
        finally
        {
            System.Threading.Monitor.Exit(handle2);
        }
    }
}

file record struct MyResource(int Number) : IDisposable
{
    public void Dispose() { }
}
```

</details>

## `try-finally` を使う場面

直接 `try-finally` を記述する場面はそんなに多くない気もしますが、糖衣構文によってコンパイラが `try-finally` に置き換えるコードはいくつかあります。

```cs
// using は try-finally の糖衣構文
using var source1 = new MyResource();

var source2 = new MyResource();
try
{
    // 省略
}
finally
{
    source2.Dispose();
}

var handle1 = new Lock();
// lock は try-finally の糖衣構文
lock (handle1)
{
    // 省略
}

var handle2 = new object();
try
{
    System.Threading.Monitor.Enter(handle2);

    // 省略
}
finally
{
    System.Threading.Monitor.Exit(handle2);
}
```

## パフォーマンス確認

```cs
// try-finally
int GetNumber(int number)
{
    try
    {
        return number * 2;
    }
    finally
    {
        ++tmp;
    }
}

var sum = 0;
for (var i = 0; i < 1000000; i++)
{
    sum += GetNumber(i);
}
```

|Test|Score|%|GC0|
|---|---:|---:|---:|
|**.NET10**||||
|InliningImprovements (4)||||
|NoInlining|52|100.0%|0|
|Inlining|442|850.0%|0|
|try-finally|439|**844.2%**|0|
|try-catch|28|53.8%|0|
|using (2)||||
|NoInlining|15|100.0%|0|
|Inlining|451|**3,006.7%**|0|
|lock (2)||||
|NoInlining|8|100.0%|0|
|Inlining|11|**137.5%**|0|
|*.NET9*||||
|InliningImprovements (4)||||
|NoInlining|53|100.0%|0|
|Inlining|439|828.3%|0|
|try-finally|32|60.4%|0|
|try-catch|30|56.6%|0|
|using (2)||||
|NoInlining|14|100.0%|0|
|Inlining|15|107.1%|0|
|lock (2)||||
|NoInlining|8|100.0%|0|
|Inlining|8|100.0%|0|

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

- x86 / x64 で機能します
- `try-catch` はインライン化されません
- `using` はインライン化によって桁違いにスコア上昇しています
- `lock` は比較的コストの大きい処理ですがインライン化されスコア上昇しています

## おわりに

今回の機能強化はランタイムの修正なので、既存のコードはそのままで最適化の恩恵を受けることができます。パフォーマンスがほしいコードを書く場合もインライン化の条件が緩和されるということで、少し書きやすくなりそうです。

[.NET10 Preview4](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)