【C# .NET 10 Preview 2】参照型がスタックに置かれる最適化

# はじめに

[.NET 10 Preview2](https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0) が利用可能になりました。

 [インライン化観測に基づく仮想化解除 / Devirtualization Based on Inlining Observations](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview2/runtime.md#devirtualization-based-on-inlining-observations) というランタイムの最適化が行われています。  
これは個人的に**ビッグ・ニュース**です。

↓ のコードで列挙のたびにオブジェクトがヒープに置かれるのが、今回の最適化でスタックに配置され（一部レジスタに配置）高速化されるというものです。
```cs
var r = GetRangeEnumerable(0, 10);
foreach (var i in r)
{
    Console.WriteLine(i);
}

static IEnumerable<int> GetRangeEnumerable(int start, int count) => new RangeEnumerable(start, count);

class RangeEnumerable(int start, int count) : IEnumerable<int>
{
    public class RangeEnumerator(int start, int count) : IEnumerator<int>
    {
        private int _value = start - 1;
        public int Current => _value;
        object IEnumerator.Current => Current;
        public void Dispose() { }
        public bool MoveNext()
        {
            _value++;
            return count-- != 0;
        }
        public void Reset() => _value = start - 1;
    }

    public IEnumerator<int> GetEnumerator() => new RangeEnumerator(start, count);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
```

## テストコード

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

```cs
using System.Collections;

public class __DevirtualizationBasedOnInliningObservationsTest
{
    static void Test(Performance p)
    {
        p.AddTest("class enumerator", () =>
        {
            var sum = 0;
            var r = GetRangeEnumerable(0, 10);
            foreach (var i in r)
                sum += i;
        });

        p.AddTest("struct enumerator", () =>
        {
            var sum = 0;
            var r = GetStructRangeEnumerable(0, 10);
            foreach (var i in r)
                sum += i;
        });

        p.AddTest("not virtual struct enumerator", () =>
        {
            var sum = 0;
            var r = new NotVirtualStructRangeEnumerable(0, 10);
            foreach (var i in r)
                sum += i;
        });

        p.AddTest("collection expression", () =>
        {
            var sum = 0;
            ReadOnlySpan<int> r = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
            foreach (var i in r)
                sum += i;
        });
    }

    static IEnumerable<int> GetRangeEnumerable(int start, int count) => new RangeEnumerable(start, count);

    static IEnumerable<int> GetStructRangeEnumerable(int start, int count) => new StructRangeEnumerable(start, count);
}

file class RangeEnumerable(int start, int count) : IEnumerable<int>
{
    public class RangeEnumerator(int start, int count) : IEnumerator<int>
    {
        private int _value = start - 1;
        public int Current => _value;
        object IEnumerator.Current => Current;
        public void Dispose() { }
        public bool MoveNext()
        {
            _value++;
            return count-- != 0;
        }
        public void Reset() => _value = start - 1;
    }

    public IEnumerator<int> GetEnumerator() => new RangeEnumerator(start, count);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

file struct StructRangeEnumerable(int start, int count) : IEnumerable<int>
{
    public struct RangeEnumerator(int start, int count) : IEnumerator<int>
    {
        private int _value = start - 1;
        public int Current => _value;
        object IEnumerator.Current => Current;
        public void Dispose() { }
        public bool MoveNext()
        {
            _value++;
            return count-- != 0;
        }
        public void Reset() => _value = start - 1;
    }

    public IEnumerator<int> GetEnumerator() => new RangeEnumerator(start, count);
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

file struct NotVirtualStructRangeEnumerable(int start, int count)
{
    public struct RangeEnumerator(int start, int count)
    {
        private int _value = start - 1;
        public int Current => _value;
        public void Dispose() { }
        public bool MoveNext()
        {
            _value++;
            return count-- != 0;
        }
        public void Reset() => _value = start - 1;
    }

    public RangeEnumerator GetEnumerator() => new RangeEnumerator(start, count);
}
```

</details>

## パフォーマンス検証

Test|Score|%|CG0|
|---|---:|---:|---:|
|**.NET10 x86**||||
|class enumerator|1,680,931|100.0%|**0**|
|struct enumerator|1,670,441|99.4%|**0**|
|not virtual struct enumerator|1,675,222|99.7%|0|
|collection expression|1,865,279|111.0%|0|
|*.NET9 x86*||||
|class enumerator|897,017|100.0%|6|
|struct enumerator|818,957|91.3%|5|
|not virtual struct enumerator|1,679,676|187.3%|0|
|collection expression|1,853,766|206.7%|0|

実行環境: Windows11 x64（ランタイムは x86）  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

- 最適化によりヒープアロケーションが削減されています
- .NET9 -> .NET10 でループのスコアが倍になっています。すごい

<details>
<summary>参考: x64 のスコア</summary>

Test|Score|%|CG0|
|---|---:|---:|---:|
|**.NET10 x64**||||
|class enumerator|289,609|100.0%|**1**|
|struct enumerator|286,508|98.9%|**1**|
|not virtual struct enumerator|755,603|260.9%|0|
|collection expression|1,118,043|386.1%|0|
|*.NET9 x64*||||
|class enumerator|301,395|100.0%|2|
|struct enumerator|284,562|94.4%|1|
|not virtual struct enumerator|771,518|256.0%|0|
|collection expression|1,102,962|366.0%|0|

実行環境: Windows11 x64  
**Score** は高いほどパフォーマンスがよいです。  
**GC0** はガベージコレクション回数を表します（少ないほどパフォーマンスがよい）。

x64 のほうはスコアがあまり変わらないようです。
</details>

## (ﾟ∀ﾟ)ｷﾀｺﾚ!!!!!!!!!!!!!!!!!!!!!!!

[以前の記事](https://qiita.com/abetakahiro123/items/2153df9795cd0f12da47#%E5%80%A4%E5%9E%8B%E3%81%AE%E9%85%8D%E5%88%97%E3%82%92%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AB%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)で小さい型をヒープではなくスタックに配置する最適化が来るかも、と予想していましたが、まさか本当にくるとは思ってなかったです。

> 確か Java には寿命が短いオブジェクトはスタックに作成する、みたいな最適化機能があったような気がするので、C# でも一般的な参照型に対象を広げられる・・・？

.NET チームは偉い！！！！！

## おわりに

[Preview2 のリリースノート](https://github.com/dotnet/core/discussions/9810)でこれを見つけたとき朝からテンションが上ってしまいました。おそらくまだ列挙パターンの小さな参照型のみ最適化の対象なのかなと思いますが、今後一般的な参照型に拡大されるかもしれません。

今回の最適化の何が偉いかというと、**ランタイムが実行時に最適化をかける**ことです。つまり既存のコードは変更しなくても恩恵があります。最適化に関するテクい書き方をしなくてもいいのは学習コスト・可読性的によろしいと思います。

関連  
[【C#】.NET 10 Preview 1 ｷﾀ━━(ﾟ∀ﾟ)━━!!](https://qiita.com/abetakahiro123/items/2153df9795cd0f12da47)  
[【C# .NET 10 Preview 1】値型の配列をスタックに作成する最適化の検証](https://qiita.com/abetakahiro123/items/9113ee6347580c5d0e39)