5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

.NET10 Preview4 が利用可能になりました。
今回はランタイムの機能強化である インライン化の改善/Inlining Improvements を確認してみます。

※ 機械翻訳です

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

テストコード

テストコード
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() { }
}

try-finally を使う場面

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

// 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);
}

パフォーマンス確認

// 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

関連
【C#】.NET10 Preview1 キタ━━(゚∀゚)━━!!
【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証
【C# .NET10 Preview2】参照型がスタックに置かれる最適化
【C# .NET10 Preview3】null 条件付き代入
【C# .NET10 Preview3】参照型の小さな配列のスタック割り当て
【C# .NET10 Preview3】拡張メソッドの機能追加

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?