はじめに
.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
は比較的コストの大きい処理ですがインライン化されスコア上昇しています
おわりに
今回の機能強化はランタイムの修正なので、既存のコードはそのままで最適化の恩恵を受けることができます。パフォーマンスがほしいコードを書く場合もインライン化の条件が緩和されるということで、少し書きやすくなりそうです。
関連
【C#】.NET10 Preview1 キタ━━(゚∀゚)━━!!
【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証
【C# .NET10 Preview2】参照型がスタックに置かれる最適化
【C# .NET10 Preview3】null 条件付き代入
【C# .NET10 Preview3】参照型の小さな配列のスタック割り当て
【C# .NET10 Preview3】拡張メソッドの機能追加