10
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 2】参照型がスタックに置かれる最適化

Posted at

はじめに

.NET 10 Preview2 が利用可能になりました。

インライン化観測に基づく仮想化解除 / Devirtualization Based on Inlining Observations というランタイムの最適化が行われています。
これは個人的にビッグ・ニュースです。

↓ のコードで列挙のたびにオブジェクトがヒープに置かれるのが、今回の最適化でスタックに配置され(一部レジスタに配置)高速化されるというものです。

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();
}

テストコード

テストコード
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);
}

パフォーマンス検証

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 でループのスコアが倍になっています。すごい
参考: x64 のスコア
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 のほうはスコアがあまり変わらないようです。

(゚∀゚)キタコレ!!!!!!!!!!!!!!!!!!!!!!!

以前の記事で小さい型をヒープではなくスタックに配置する最適化が来るかも、と予想していましたが、まさか本当にくるとは思ってなかったです。

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

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

おわりに

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

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

関連
【C#】.NET 10 Preview 1 キタ━━(゚∀゚)━━!!
【C# .NET 10 Preview 1】値型の配列をスタックに作成する最適化の検証

10
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
10
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?