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

# はじめに

今回は、[前回の .NET 10 Preview 1 の新機能紹介記事](https://qiita.com/abetakahiro123/items/2153df9795cd0f12da47) の、機能別の検証です。

個人的にとてもホットな新機能 [Stack Allocation of Arrays of Value Types](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/runtime.md#stack-allocation-of-arrays-of-value-types) を深堀りしてみます。  
（↓ の配列がヒープではなくスタックに作成されるという最適化）

```cs
int[] numbers = {1, 2, 3};
int sum = 0;

for (int i = 0; i < numbers.Length; i++)
{
    sum += numbers[i];
}

Console.WriteLine(sum);
```

## サンプルコード

<details>
<summary>テストコード</summary>

```cs
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;
        });
    }
}
```

</details>

## パフォーマンステスト

|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** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

<details>
<summary>.NET 10 x64</summary>

|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  
</details>

<details>
<summary>.NET 9 x86</summary>

|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  

</details>

<details>
<summary>.NET 9 x64</summary>

|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  
</details>

## わかったこと

**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` のがパフォーマンスよくね？
  - ・・・

## おわりに

今のところ[**コレクション式使っとけ**](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/collection-expressions)というのが結論で、今回の最適化はかなり限定的です。実行時に長さが決まる `unmanaged` 型のスパンが欲しい場合は、引き続き `stackalloc` になりそうです。

[System.IO.Stream.Read()](https://learn.microsoft.com/ja-jp/dotnet/api/system.io.stream.read?view=net-9.0) 等の配列を引数に取るメソッドで活用できるかもしれません。  
関連：[【C#】Stream.Read/Write(Span<byte>) のパフォーマンス](https://qiita.com/abetakahiro123/items/ebdcc3f01c4124ede626)

一方で参照型をスタックに配置する試み（`int[]` は参照型）は目を引くもので、C# がずっと手を付けなかった分野です。非常に興味深いので、今後の動向を注視しています👀。