【C# .NET10 Preview3】参照型の小さな配列のスタック割り当て

# はじめに

[.NET10 Preview3](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0) 関連記事の続きです。  
[リリースノート](https://github.com/dotnet/core/discussions/9846)

今回はランタイム機能である[参照型の小さな配列のスタック割り当て / Stack Allocation of Small Arrays of Reference Types](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview3/runtime.md#stack-allocation-of-small-arrays-of-reference-types) を検証しようと思います。

Preview1 では**値型の**配列をスタックに割り当ててヒープアロケーションを削減していました。今回は**参照型の**配列もスタックに割り当てられるようになりました。

```cs
string[] words = {"Hello", "World!"};
foreach (var str in words)
    Console.WriteLine(str);
```

## サンプルコード

<details>
<summary>サンプルコード</summary>

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

</details>

## パフォーマンス計測

:::note warn
対象 CPU は x86
例のごとく **x86** でしか最適化はかかりません
:::

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

- 最適化によりヒープアロケーションが削減されています
- 最速であるとされるコレクション式と同水準のパフォーマンスです

## 配列を引数に渡した場合はヒープになる

```cs
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` で参照が漏れない場合も最適化されません
- この辺は興味深く、今後は最適化の余地がありそうです

## 参照型を含む値型の配列もスタックに割り当て

```cs
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# が当初から目指す方針に向かっているようです。

[.NET10 Preview3](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0)

関連  
[【C#】.NET10 Preview1 ｷﾀ━━(ﾟ∀ﾟ)━━!!](https://qiita.com/abetakahiro123/items/2153df9795cd0f12da47)  
[【C# .NET10 Preview1】値型の配列をスタックに作成する最適化の検証](https://qiita.com/abetakahiro123/items/9113ee6347580c5d0e39)  
[【C# .NET10 Preview2】参照型がスタックに置かれる最適化](https://qiita.com/abetakahiro123/items/cedd77ba25de1b8db8d1)  
[【C# .NET10 Preview3】null 条件付き代入](https://qiita.com/abetakahiro123/items/db05ae0cab986183cfab)