略して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