3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C# .NET 10 Preview 1】値型の配列をスタックに作成する最適化の検証

Last updated at Posted at 2025-03-10

はじめに

今回は、前回の .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# がずっと手を付けなかった分野です。非常に興味深いので、今後の動向を注視しています👀。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?