はじめに
.NET 10 Preview2 が利用可能になりました。
インライン化観測に基づく仮想化解除 / Devirtualization Based on Inlining Observations というランタイムの最適化が行われています。
これは個人的にビッグ・ニュースです。
↓ のコードで列挙のたびにオブジェクトがヒープに置かれるのが、今回の最適化でスタックに配置され(一部レジスタに配置)高速化されるというものです。
var r = GetRangeEnumerable(0, 10);
foreach (var i in r)
{
Console.WriteLine(i);
}
static IEnumerable<int> GetRangeEnumerable(int start, int count) => new RangeEnumerable(start, count);
class RangeEnumerable(int start, int count) : IEnumerable<int>
{
public class RangeEnumerator(int start, int count) : IEnumerator<int>
{
private int _value = start - 1;
public int Current => _value;
object IEnumerator.Current => Current;
public void Dispose() { }
public bool MoveNext()
{
_value++;
return count-- != 0;
}
public void Reset() => _value = start - 1;
}
public IEnumerator<int> GetEnumerator() => new RangeEnumerator(start, count);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
テストコード
テストコード
using System.Collections;
public class __DevirtualizationBasedOnInliningObservationsTest
{
static void Test(Performance p)
{
p.AddTest("class enumerator", () =>
{
var sum = 0;
var r = GetRangeEnumerable(0, 10);
foreach (var i in r)
sum += i;
});
p.AddTest("struct enumerator", () =>
{
var sum = 0;
var r = GetStructRangeEnumerable(0, 10);
foreach (var i in r)
sum += i;
});
p.AddTest("not virtual struct enumerator", () =>
{
var sum = 0;
var r = new NotVirtualStructRangeEnumerable(0, 10);
foreach (var i in r)
sum += i;
});
p.AddTest("collection expression", () =>
{
var sum = 0;
ReadOnlySpan<int> r = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
foreach (var i in r)
sum += i;
});
}
static IEnumerable<int> GetRangeEnumerable(int start, int count) => new RangeEnumerable(start, count);
static IEnumerable<int> GetStructRangeEnumerable(int start, int count) => new StructRangeEnumerable(start, count);
}
file class RangeEnumerable(int start, int count) : IEnumerable<int>
{
public class RangeEnumerator(int start, int count) : IEnumerator<int>
{
private int _value = start - 1;
public int Current => _value;
object IEnumerator.Current => Current;
public void Dispose() { }
public bool MoveNext()
{
_value++;
return count-- != 0;
}
public void Reset() => _value = start - 1;
}
public IEnumerator<int> GetEnumerator() => new RangeEnumerator(start, count);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
file struct StructRangeEnumerable(int start, int count) : IEnumerable<int>
{
public struct RangeEnumerator(int start, int count) : IEnumerator<int>
{
private int _value = start - 1;
public int Current => _value;
object IEnumerator.Current => Current;
public void Dispose() { }
public bool MoveNext()
{
_value++;
return count-- != 0;
}
public void Reset() => _value = start - 1;
}
public IEnumerator<int> GetEnumerator() => new RangeEnumerator(start, count);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
file struct NotVirtualStructRangeEnumerable(int start, int count)
{
public struct RangeEnumerator(int start, int count)
{
private int _value = start - 1;
public int Current => _value;
public void Dispose() { }
public bool MoveNext()
{
_value++;
return count-- != 0;
}
public void Reset() => _value = start - 1;
}
public RangeEnumerator GetEnumerator() => new RangeEnumerator(start, count);
}
パフォーマンス検証
Test | Score | % | CG0 |
---|---|---|---|
.NET10 x86 | |||
class enumerator | 1,680,931 | 100.0% | 0 |
struct enumerator | 1,670,441 | 99.4% | 0 |
not virtual struct enumerator | 1,675,222 | 99.7% | 0 |
collection expression | 1,865,279 | 111.0% | 0 |
.NET9 x86 | |||
class enumerator | 897,017 | 100.0% | 6 |
struct enumerator | 818,957 | 91.3% | 5 |
not virtual struct enumerator | 1,679,676 | 187.3% | 0 |
collection expression | 1,853,766 | 206.7% | 0 |
実行環境: Windows11 x64(ランタイムは x86)
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- 最適化によりヒープアロケーションが削減されています
- .NET9 -> .NET10 でループのスコアが倍になっています。すごい
参考: x64 のスコア
Test | Score | % | CG0 |
---|---|---|---|
.NET10 x64 | |||
class enumerator | 289,609 | 100.0% | 1 |
struct enumerator | 286,508 | 98.9% | 1 |
not virtual struct enumerator | 755,603 | 260.9% | 0 |
collection expression | 1,118,043 | 386.1% | 0 |
.NET9 x64 | |||
class enumerator | 301,395 | 100.0% | 2 |
struct enumerator | 284,562 | 94.4% | 1 |
not virtual struct enumerator | 771,518 | 256.0% | 0 |
collection expression | 1,102,962 | 366.0% | 0 |
実行環境: Windows11 x64
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
x64 のほうはスコアがあまり変わらないようです。
(゚∀゚)キタコレ!!!!!!!!!!!!!!!!!!!!!!!
以前の記事で小さい型をヒープではなくスタックに配置する最適化が来るかも、と予想していましたが、まさか本当にくるとは思ってなかったです。
確か Java には寿命が短いオブジェクトはスタックに作成する、みたいな最適化機能があったような気がするので、C# でも一般的な参照型に対象を広げられる・・・?
.NET チームは偉い!!!!!
おわりに
Preview2 のリリースノートでこれを見つけたとき朝からテンションが上ってしまいました。おそらくまだ列挙パターンの小さな参照型のみ最適化の対象なのかなと思いますが、今後一般的な参照型に拡大されるかもしれません。
今回の最適化の何が偉いかというと、ランタイムが実行時に最適化をかけることです。つまり既存のコードは変更しなくても恩恵があります。最適化に関するテクい書き方をしなくてもいいのは学習コスト・可読性的によろしいと思います。
関連
【C#】.NET 10 Preview 1 キタ━━(゚∀゚)━━!!
【C# .NET 10 Preview 1】値型の配列をスタックに作成する最適化の検証