【C#】stackallocでstructをスタックに確保してパフォーマンスを上げよう戦略

このエントリーをはてなブックマークに追加

略してstackalloc戦略です。

前回の記事でin引数戦略(最終的にはref戦略な感じになりましたけど)を提示してみましたが、in引数戦略では使える幅が狭いです。

なぜかというと、配列が使えないから。

.NETでは配列はヒープに確保されるので、原理上、配列の値をローカル変数に持ってくると値のコピーが発生してしまいます。 それではin引数戦略はほとんどの場合で使い物になりません。

そこでstackalloc戦略です。 C# 7.2で安全なstackallocであればunsafeコンテキスト以外で使用できるようになりました。安全というのはSpan<T>と併用している場合です。 (Span<T>は.NET Core 2.1以外ではSystem.Memoryパッケージが必要とされます)

stackallocを用いてスタック上に配列のようなもの(Span<T>)を確保することで、in引数戦略の効力を高めようという感じです。

肝心のベンチマーク

今回もBenchmarkDotNetを使用しています。
今回は.NET Core 2.0と2.1のパフォーマンス差も確認したいので、csprojは以下のような感じの記述を追加しています。

    <TargetFrameworks>netcoreapp2.0;netcoreapp2.1;net47</TargetFrameworks>
    <PlatformTarget>AnyCPU</PlatformTarget>

また、CoreJobもそのままでは使えないので、新しく属性を定義してやります。

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
    public class Core20JobAttribute : Attribute, IConfigSource
    {
        public Core20JobAttribute()
        {
            Config = ManualConfig.CreateEmpty().With(Job.Core.With(CsProjCoreToolchain.NetCoreApp20));
        }

        public IConfig Config { get; }
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
    public class Core21JobAttribute : Attribute, IConfigSource
    {
        public Core21JobAttribute()
        {
            Config = ManualConfig.CreateEmpty().With(Job.Core.With(CsProjCoreToolchain.NetCoreApp21));
        }

        public IConfig Config { get; }
    }

そしてベンチマーク対象はこんな感じのメソッドです。

        [Benchmark]
        public int NormalIndexOf()
        {
            int[] ar = new[] { 1, 4, 7, 8, 0, 1 };
            for (int i = 0; i < ar.Length; i++)
            {
                if (ar[i] == 0)
                {
                    return i;
                }
            }
            return -1;
        }

        [Benchmark]
        public int SpanIndexOf()
        {
            Span ar = stackalloc[] { 1, 4, 7, 8, 0, 1 };
            for (int i = 0; i < ar.Length; i++)
            {
                if (ar[i] == 0)
                {
                    return i;
                }
            }
            return -1;
        }

そして、ついでにin引数戦略っぽいベンチマークもしましょう。

        public readonly struct Size8
        {
            public readonly int A, B;

            public Size8(int a, int b)
            {
                A = a;
                B = b;
            }
        }

        [Benchmark]
        public Size8 NormalMax8()
        {
            Span ar = new[] {
                new Size8(1, 2),
                new Size8(2, 2),
                new Size8(3, 3),
                new Size8(1, 1)
            };

            Size8 max = ar[0];
            for (int i = 1; i < ar.Length; i++)
            {
                Size8 x = ar[i];
                if (max.A + max.B < x.A + x.B)
                {
                    max = x;
                }
            }
            return max;
        }

        [Benchmark]
        public Size8 SpanMax8()
        {
            Span ar = stackalloc[] {
                new Size8(1, 2),
                new Size8(2, 2),
                new Size8(3, 3),
                new Size8(1, 1)
            };

            ref Size8 max = ref ar[0];
            for (int i = 1; i < ar.Length; i++)
            {
                ref Size8 x = ref ar[i];
                if (max.A + max.B < x.A + x.B)
                {
                    max = ref x;
                }
            }
            return max;
        }

そしてこれのSize16版もしました。 ソースコード全体はgistに上げています。

そしてベンチマーク結果ですが、.NET Core 2.1つょぃという結果に。

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.492 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
Frequency=3328123 Hz, Resolution=300.4697 ns, Timer=TSC
.NET Core SDK=2.1.300
  [Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  Job-ANIBAB : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  Job-TCMRRU : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
  Clr        : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2671.0

.NET Core 2.1ではstackalloc/Spanが早い結果が出ましたが、その他では致命的に遅いという結果に。。。

これは、.NET Core 2.1以外ではSpan<T>に対する特殊対応が入っていないからと推測できます。

.NET Core 2.1ならstackalloc戦略は有効か?

一概にもそういうわけではないです。そもそもスタック領域はあまり容量のない領域ですので、それなりに大きいサイズのstackallocはしないほうがいいかと思います。 (いろいろ調べてみるとスタック領域は標準で1MBらしいです)

まとめ

  • .NET Core 2.1は強かった
  • .NET Core 2.1ならstackalloc戦略は使えそう
  • しかし、現状は.NET Core 2.1以外の環境を考えると使いどころに悩む
  • 容量用法を正しく守ってin引数戦略・stackalloc戦略を採りましょうね

今回は結構ガバガバなベンチマークを取っていますが、もっといいベンチマークを取ってみたよ!などがあればご一報くださいm(__)m