Intrinsicsを使ってdouble配列の合計値を計算を高速化
今更感があふれているのですが、System.Runtime.Intrinsics
のAVX命令を使ってdouble[]
の合計値計算処理を高速化してみます。
仕事では何かと数値計算処理を実装する機会が多いのですが、ふと「そういえばHardware Intrinsics機能がずいぶん前に追加されてたなー あれ効果あるのかなー」と気になったので、ちょっと軽くベンチを取ってみました。
とりあえずdouble[]
の合計値を計算するというお題でやってみます。
早速調べてみたところ、Avx2
クラスがVector
構造体を基本にして様々な計算処理を提供している模様。
Vector
構造体もビット長に応じて64、128、256と3種類が提供されているようです。double
型はサイズが大きいので、当然使う構造体はVector256
になります。
ヘルパーの作成
何はともあれ、double
の配列を処理するのですからまずは配列からVector
構造体を生成するヘルパーを定義します。参考にした記事によるとどうやらunsafeじゃないといけないということで、以下のようにサクッと作ります。
public static class VectorFactory
{
public static unsafe Vector256<double> FromArray(double[] array, int index)
{
fixed (double* p = array)
{
return Avx2.LoadVector256(p + index);
}
}
}
合計値計算処理を実装
そして早速合計値を計算する処理を実装してみましょう。普通に配列をfor文で回したときと、Intrinsicsを使ったときの2パターンを実装します。
public static class Computer
{
public static double SumArray(double[] array)
{
var sum = 0d;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
public static double SumAvx(double[] array)
{
var accum = Vector256<double>.Zero;
for (int i = 0; i < array.Length; i += 4)
{
var v = VectorFactory.FromArray(array, i);
accum = Avx2.Add(accum, v);
}
var v0 = accum.GetElement(0);
var v1 = accum.GetElement(1);
var v2 = accum.GetElement(2);
var v3 = accum.GetElement(3);
return v0 + v1 + v2 + v3;
}
}
ベンチマークコードの実装
ベンチでは1024*1024
のランダムな配列の合計値を計算させてみます。一応おまけでLINQのSum
オペレーターを使った結果もベンチに含めています。
さて… どれくらいの差が出るのでしょうか…?
public class Bench
{
private readonly double[] a;
public Bench()
{
var rand = new Random();
a = Enumerable.Range(0, 1024 * 1024)
.Select(x => 100 * rand.NextDouble())
.ToArray();
}
[Benchmark(Baseline = true)]
public double SumArray()
{
return Computer.SumArray(a);
}
[Benchmark]
public double SumLinq()
{
return a.Sum();
}
[Benchmark]
public double SumAvx()
{
return Computer.SumAvx(a);
}
}
結果
というわけで結果は以下の通りです。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
11th Gen Intel Core i5-11400F 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=5.0.402
[Host] : .NET 5.0.11 (5.0.1121.47308), X64 RyuJIT
DefaultJob : .NET 5.0.11 (5.0.1121.47308), X64 RyuJIT
| Method | Mean | Error | StdDev | Ratio | RatioSD |
|--------- |-----------:|---------:|---------:|------:|--------:|
| SumArray | 1,043.0 us | 20.54 us | 23.65 us | 1.00 | 0.00 |
| SumLinq | 3,250.8 us | 26.23 us | 21.90 us | 3.13 | 0.07 |
| SumAvx | 325.1 us | 6.14 us | 11.68 us | 0.31 | 0.01 |
いいですね。3倍以上に高速化されました。
実際の業務では合計値だけでなく様々な統計量や畳み込み処理を行うので、ここまで簡単な実装で使えるわけではないですが、何か適用できるところがないか探してみたいですね。
Discussion