はじめに
.NET10 Preview3 関連記事の続きです。
リリースノート
今回はランタイム機能である参照型の小さな配列のスタック割り当て / Stack Allocation of Small Arrays of Reference Types を検証しようと思います。
Preview1 では値型の配列をスタックに割り当ててヒープアロケーションを削減していました。今回は参照型の配列もスタックに割り当てられるようになりました。
string[] words = {"Hello", "World!"};
foreach (var str in words)
Console.WriteLine(str);
サンプルコード
サンプルコード
file record struct Person(string Name, int Age);
public class __StackAllocationOfSmallArraysOfReferenceTypesTest
{
static void ClosedScope(Performance p)
{
p.AddTest("Array", () =>
{
string[] words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
var sum = 0;
foreach (var n in words)
sum += n.Length;
});
p.AddTest("ReadOnlySpan", () =>
{
ReadOnlySpan<string> words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
var sum = 0;
foreach (var n in words)
sum += n.Length;
});
}
static void UseMethod(Performance p)
{
static int Sum(scoped ReadOnlySpan<string> words)
{
var result = 0;
foreach (var n in words)
result += n.Length;
return result;
}
p.AddTest("Array", () =>
{
string[] words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
Sum(words);
});
p.AddTest("ReadOnlySpan", () =>
{
ReadOnlySpan<string> words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
Sum(words);
});
}
static void PersonValueType(Performance p)
{
p.AddTest("Array", () =>
{
Person[] people = [
new("Alice", 30),
new("Bob", 25),
new("Charlie", 35),
new("Diana", 28),
new("Eve", 22)
];
var sum = 0;
foreach (var n in people)
sum += n.Age + n.Name.Length;
});
p.AddTest("ReadOnlySpan", () =>
{
ReadOnlySpan<Person> people = [
new("Alice", 30),
new("Bob", 25),
new("Charlie", 35),
new("Diana", 28),
new("Eve", 22)
];
var sum = 0;
foreach (var n in people)
sum += n.Age + n.Name.Length;
});
}
}
パフォーマンス計測
対象 CPU は x86
例のごとく x86 でしか最適化はかかりません
// Array
string[] words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
var sum = 0;
foreach (var n in words)
sum += n.Length;
// ReadOnlySpan
ReadOnlySpan<string> words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
var sum = 0;
foreach (var n in words)
sum += n.Length;
Test | Score | % | CG0 |
---|---|---|---|
.NET10 x86 | |||
Array | 1,854,908 | 100.0% | 0 |
ReadOnlySpan | 1,840,533 | 99.2% | 0 |
.NET9 x86 | |||
Array | 1,689,021 | 100.0% | 10 |
ReadOnlySpan | 1,838,107 | 108.8% | 0 |
実行環境: Windows11 x64(ランタイムは x86)
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
- 最適化によりヒープアロケーションが削減されています
- 最速であるとされるコレクション式と同水準のパフォーマンスです
配列を引数に渡した場合はヒープになる
static int Sum(scoped ReadOnlySpan<string> words)
{
var result = 0;
foreach (var n in words)
result += n.Length;
return result;
}
// Array
string[] words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
Sum(words);
// ReadOnlySpan
ReadOnlySpan<string> words =
["Hello", "World!", "C#", "dotnet", ".NET10"];
Sum(words);
Test | Score | % | CG0 |
---|---|---|---|
.NET10 x86 | |||
Array | 1,311,259 | 100.0% | 8 |
ReadOnlySpan | 1,496,933 | 114.2% | 0 |
.NET9 x86 | |||
Array | 1,329,545 | 100.0% | 8 |
ReadOnlySpan | 1,455,755 | 109.5% | 0 |
- 配列の参照が他のスコープに漏れる場合、最適化の対象外のようです
-
ReadOnlySpan
等の、読み取り専用で引数に渡した場合も最適化されません -
scoped
で参照が漏れない場合も最適化されません - この辺は興味深く、今後は最適化の余地がありそうです
参照型を含む値型の配列もスタックに割り当て
record struct Person(string Name, int Age);
Test | Score | % | CG0 |
---|---|---|---|
.NET10 x86 | |||
Array | 1,794,758 | 100.0% | 0 |
ReadOnlySpan | 1,741,521 | 97.0% | 0 |
.NET9 x86 | |||
Array | 1,614,818 | 100.0% | 16 |
ReadOnlySpan | 1,686,389 | 104.4% | 0 |
- 最適化によりヒープアロケーションが削減されています
- 参照型を含む値型の配列も、今回最適化の対象になったようです
おわりに
キキキキキキキタタタタタ─────((((゚゚゚∀∀゚゚゚゚)))))─────!!!!!!!!
.NET10 はスタック系の最適化が大変よろしいです。この最適化はランタイム機能のためそれをアテにしたコードを書くべきかはどうかですが、「コードは簡潔に保ってランタイムが最適化をかける」という、C# が当初から目指す方針に向かっているようです。
関連
【C#】.NET10 Preview1 キタ━━(゚∀゚)━━!!
【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証
【C# .NET10 Preview2】参照型がスタックに置かれる最適化
【C# .NET10 Preview3】null 条件付き代入