はじめに
今回は、前回の .NET 10 Preview 1 の新機能紹介記事 の、機能別の検証です。
個人的にとてもホットな新機能 Stack Allocation of Arrays of Value Types を深堀りしてみます。
(↓ の配列がヒープではなくスタックに作成されるという最適化)
int[] numbers = {1, 2, 3};
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
Console.WriteLine(sum);
サンプルコード
テストコード
public class __StackAllocationOfArraysOfValueTypesTest
{
static void Dummy(Performance p)
{
p.AddTest("Dummy", () => { });
}
static void FixedArray(Performance p)
{
p.AddTest("{ 1, 2, 3 }", () =>
{
int[] numbers = { 1, 2, 3 };
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("{ \"1\", \"2\", \"3\" }", () =>
{
string[] numbers = { "1", "2", "3" };
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i].Length;
});
p.AddTest("[1, 2, 3]", () =>
{
ReadOnlySpan<int> numbers = [1, 2, 3];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("[\"1\", \"2\", \"3\"]", () =>
{
ReadOnlySpan<string> numbers = ["1", "2", "3"];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i].Length;
});
p.AddTest("stackalloc { 1, 2, 3 }", () =>
{
var numbers = (stackalloc int[] { 1, 2, 3 });
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
}
static void Array(Performance p)
{
var length = 3;
p.AddTest("int[3]", () =>
{
int[] numbers = new int[length];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("string[3]", () =>
{
string[] numbers = new string[length];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i]?.Length ?? 0;
});
p.AddTest("stackalloc[3]", () =>
{
var numbers = (stackalloc int[length]);
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
}
static void ConstArray(Performance p)
{
const int Length = 3;
p.AddTest("int[3]", () =>
{
int[] numbers = new int[Length];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("string[3]", () =>
{
string[] numbers = new string[Length];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i]?.Length ?? 0;
});
p.AddTest("stackalloc[3]", () =>
{
var numbers = (stackalloc int[Length]);
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
}
static void StructType(Performance p)
{
const int Length = 3;
p.AddTest("(int, int)[3]", () =>
{
(int, int)[] numbers = new (int, int)[Length];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i].Item1;
});
p.AddTest("(string, int)[3]", () =>
{
(string, int)[] numbers = new (string, int)[Length];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i].Item1?.Length ?? 0;
});
}
static void LongArray(Performance p)
{
p.AddTest("int[130]", () =>
{
int[] numbers = new int[130];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("int[131]", () =>
{
int[] numbers = new int[131];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("decimal[32]", () =>
{
decimal[] numbers = new decimal[32];
decimal sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("decimal[33]", () =>
{
decimal[] numbers = new decimal[33];
decimal sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("byte[1000]", () =>
{
byte[] numbers = new byte[1000];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("sbyte[1000]", () =>
{
sbyte[] numbers = new sbyte[1000];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i];
});
p.AddTest("bool[1000]", () =>
{
bool[] numbers = new bool[1000];
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += numbers[i] ? 1 : 0;
});
}
}
パフォーマンステスト
Test | Score | % | CG0 |
---|---|---|---|
FixedArray (5) | |||
{ 1, 2, 3 } | 1,841,566 | 100.0% | 0 |
{ "1", "2", "3" } | 1,668,437 | 90.6% | 7 |
[1, 2, 3] | 2,076,832 | 112.8% | 0 |
["1", "2", "3"] | 2,213,023 | 120.2% | 0 |
stackalloc { 1, 2, 3 } | 2,039,720 | 110.8% | 0 |
Array (3) | |||
int[3] | 2,164,137 | 100.0% | 9 |
string[3] | 2,153,380 | 99.5% | 9 |
stackalloc[3] | 2,218,186 | 102.5% | 0 |
ConstArray (3) | |||
int[3] | 2,281,755 | 100.0% | 0 |
string[3] | 2,137,546 | 93.7% | 9 |
stackalloc[3] | 2,155,951 | 94.5% | 0 |
StructType (2) | |||
(int, int)[3] | 2,289,387 | 100.0% | 0 |
(string, int)[3] | 2,132,836 | 93.2% | 14 |
LongArray (7) | |||
int[130] | 1,403,012 | 100.0% | 0 |
int[131] | 1,183,399 | 84.3% | 121 |
decimal[32] | 365,495 | 26.1% | 0 |
decimal[33] | 368,396 | 26.3% | 38 |
byte[1000] | 374,644 | 26.7% | 0 |
sbyte[1000] | 369,878 | 26.4% | 0 |
bool[1000] | 361,073 | 25.7% | 0 |
実行環境: Windows11 x86 .NET Runtime 10.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
.NET 10 x64
Test | Score | % | CG0 |
---|---|---|---|
FixedArray (5) | |||
{ 1, 2, 3 } | 831,579 | 100.0% | 11 |
{ "1", "2", "3" } | 1,903,785 | 228.9% | 10 |
[1, 2, 3] | 2,010,833 | 241.8% | 0 |
["1", "2", "3"] | 1,942,311 | 233.6% | 0 |
stackalloc { 1, 2, 3 } | 2,873,128 | 345.5% | 0 |
Array (3) | |||
int[3] | 2,287,125 | 100.0% | 10 |
string[3] | 2,194,877 | 96.0% | 12 |
stackalloc[3] | 3,048,620 | 133.3% | 0 |
ConstArray (3) | |||
int[3] | 2,293,999 | 100.0% | 10 |
string[3] | 2,145,233 | 93.5% | 12 |
stackalloc[3] | 3,087,762 | 134.6% | 0 |
StructType (2) | |||
(int, int)[3] | 2,271,749 | 100.0% | 13 |
(string, int)[3] | 2,078,791 | 91.5% | 17 |
LongArray (7) | |||
int[130] | 218,416 | 100.0% | 14 |
int[131] | 216,775 | 99.2% | 14 |
decimal[32] | 267,562 | 122.5% | 17 |
decimal[33] | 259,411 | 118.8% | 17 |
byte[1000] | 28,361 | 13.0% | 3 |
sbyte[1000] | 28,336 | 13.0% | 3 |
bool[1000] | 28,390 | 13.0% | 3 |
実行環境: Windows11 x64 .NET Runtime 10.0.0
.NET 9 x86
Test | Score | % | CG0 |
---|---|---|---|
FixedArray (5) | |||
{ 1, 2, 3 } | 1,749,954 | 100.0% | 8 |
{ "1", "2", "3" } | 1,666,842 | 95.3% | 7 |
[1, 2, 3] | 2,186,218 | 124.9% | 0 |
["1", "2", "3"] | 2,149,135 | 122.8% | 0 |
stackalloc { 1, 2, 3 } | 2,110,043 | 120.6% | 0 |
Array (3) | |||
int[3] | 2,112,894 | 100.0% | 9 |
string[3] | 2,203,934 | 104.3% | 10 |
stackalloc[3] | 2,194,432 | 103.9% | 0 |
ConstArray (3) | |||
int[3] | 2,162,979 | 100.0% | 9 |
string[3] | 2,166,691 | 100.2% | 9 |
stackalloc[3] | 2,124,667 | 98.2% | 0 |
StructType (2) | |||
(int, int)[3] | 2,147,604 | 100.0% | 14 |
(string, int)[3] | 2,156,154 | 100.4% | 14 |
LongArray (7) | |||
int[130] | 1,175,389 | 100.0% | 119 |
int[131] | 1,173,133 | 99.8% | 120 |
decimal[32] | 363,854 | 31.0% | 36 |
decimal[33] | 370,696 | 31.5% | 38 |
byte[1000] | 369,373 | 31.4% | 0 |
sbyte[1000] | 364,194 | 31.0% | 0 |
bool[1000] | 363,858 | 31.0% | 0 |
実行環境: Windows11 x86 .NET Runtime 9.0.0
.NET 9 x64
Test | Score | % | CG0 |
---|---|---|---|
FixedArray (5) | |||
{ 1, 2, 3 } | 582,161 | 100.0% | 7 |
{ "1", "2", "3" } | 1,736,927 | 298.4% | 9 |
[1, 2, 3] | 1,817,891 | 312.3% | 0 |
["1", "2", "3"] | 1,676,433 | 288.0% | 0 |
stackalloc { 1, 2, 3 } | 2,411,426 | 414.2% | 0 |
Array (3) | |||
int[3] | 2,128,744 | 100.0% | 10 |
string[3] | 1,882,225 | 88.4% | 10 |
stackalloc[3] | 2,919,083 | 137.1% | 0 |
ConstArray (3) | |||
int[3] | 2,135,884 | 100.0% | 10 |
string[3] | 1,905,708 | 89.2% | 10 |
stackalloc[3] | 2,981,939 | 139.6% | 0 |
StructType (2) | |||
(int, int)[3] | 2,127,896 | 100.0% | 12 |
(string, int)[3] | 1,920,627 | 90.3% | 16 |
LongArray (7) | |||
int[130] | 201,729 | 100.0% | 13 |
int[131] | 199,879 | 99.1% | 13 |
decimal[32] | 249,680 | 123.8% | 16 |
decimal[33] | 241,638 | 119.8% | 15 |
byte[1000] | 26,665 | 13.2% | 3 |
sbyte[1000] | 26,273 | 13.0% | 3 |
bool[1000] | 25,963 | 12.9% | 3 |
実行環境: Windows11 x64 .NET Runtime 9.0.0
わかったこと
Preview 1 時点の結果です(重要)。今後変更になる可能性が高いです。
値型の種類 | x86 | x64 |
---|---|---|
unmanaged |
◯ | ✕ |
managed |
✕ | ✕ |
-
x86 でしか最適化がかからない
- 手元の環境では x86 でしか最適化がかかりませんでした。これは今後他のアーキテクチャでも最適化がかかるようになるでしょう。x64/arm64 でも動く x86 を優先して実装したんだと思います。
- 最適化は
unmanaged
限定-
stackalloc
に参照型を含んでしまうとガベージコレクションが大変なので、かえって遅くなるらしいです。今回の最適化も根っこは同じものと思われます。 - 参考: https://ufcpp.net/blog/2022/2/params-span/
- 個人的にはここを期待していたのですが、やはり難しい模様。
-
-
コンパイル時に長さが確定する配列のみ対象
-
int length = 3; new int[length]
では最適化がかかりません。const int Length = 3;
とかなら最適化がかかります。つまり最適化のためのコード解析がコンパイル時です。 - これは今後のアップデートで実行時(JIT コンパイル時)に最適化してくれるのを期待したいところ。
-
- スタックに配置する配列のサイズの境界値は 520Byte くらい
-
int
だと 130 までです。128(きりの良い数字)より少し多め。 - これは実行環境依存かもしれません。ここの判断をランタイムに丸投げできるようになったため、
Span<int> numbers = length < 100 ? stackalloc int[length] : new int[length]
みたいな分岐を省略できます。
-
- 小さい型はとても長くてもスタックに配置する(
sizeof(T) == 1
の型?)-
new byte[10000]
等とても長い配列でもスタックに配置されます。 - C# では Utf8 文字列を
ReadOnlySpan<byte>
で表現するため、特需ということでしょうか。
-
-
stackalloc
のがパフォーマンスよくね?- ・・・
おわりに
今のところコレクション式使っとけというのが結論で、今回の最適化はかなり限定的です。実行時に長さが決まる unmanaged
型のスパンが欲しい場合は、引き続き stackalloc
になりそうです。
System.IO.Stream.Read() 等の配列を引数に取るメソッドで活用できるかもしれません。
関連:【C#】Stream.Read/Write(Span) のパフォーマンス
一方で参照型をスタックに配置する試み(int[]
は参照型)は目を引くもので、C# がずっと手を付けなかった分野です。非常に興味深いので、今後の動向を注視しています👀。