「System.Numerics.Vector<T>」の版間の差分
Administrator (トーク | 投稿記録) (→ベンチマーク) |
Administrator (トーク | 投稿記録) (→使ってみる) |
||
(同じ利用者による、間の1版が非表示) | |||
54行目: | 54行目: | ||
== 使ってみる == | == 使ってみる == | ||
− | + | 基本は以下の繰り返し。 | |
+ | * 配列の一部分をVector<T>に格納する | ||
+ | * ベクトル演算を実行する | ||
+ | * 演算結果のVector<T>を配列に書き戻す | ||
+ | |||
<source lang="csharp"> | <source lang="csharp"> | ||
// 配列を用意する | // 配列を用意する |
2020年5月27日 (水) 04:59時点における最新版
System.Numerics.Vector<T>はSIMDレジスタを表す不変の構造体である。
こいつを使うと.NETのランタイムが上手いこと解釈してSIMDで加速される。
なお「SIMD演算器がある場合に」と注釈があり、加速されないこともあるようなことが書いてあるが、 Android最初期のT-01Cなどに搭載されたSnapdragon S1(QSD8250)ですらSIMD(NEON)を搭載しているわけで、 いまどきSIMDが搭載されていないCPUは存在するのか謎である。 というか、それより下のクラスの組み込み系CPUで.NETがまともに動くのか謎である。
Vector<T>はVector<T>.Countの大きさの配列のようなものである。 Vector<int>は「intが8個の配列」のような感じだ。
あとは普通に四則演算などをすると内部でベクトル演算が行われる。
var v1 = new Vector<int>(1,2,3,4,5,6,7,8);
var v2 = new Vector<int>(1,2,3,4,5,6,7,8);
// ベクトル演算が行われる
var v3 = v1 + v2;
// v3 = <2, 4, 6, 8, 10, 12, 14, 16>
四則演算だけでなくVectorクラスにはMinやMaxやDotなども用意されている。
Vector<T>.Countプロパティ[編集 | ソースを編集]
1つのVector<T>構造体に格納可能な変数の数を得られる。 CPUのSIMDレジスタの幅だな。
// SIMDレジスタにintを格納できる数
var regs = Vector<int>.Count;
// intのビット数
var bits = sizeof(int) * 8;
// SIMDレジスタのビット数
var simd = bits * count;
Console.WriteLine($"{simd}bit = {bits}bit * {regs}unit");
// 256bit = 32bit * 8unit
Vector<T>のコンストラクタ[編集 | ソースを編集]
引数1つ指定するとVector<T>.Countの数だけ埋め尽くされる。
var v = new Vector<int>(1); // v = <1,1,1,1,1,1,1,1>;
引数に配列を指定するとVector<T>.Countの数だけ埋め尽くされる。
var v = new Vector<int>(new[]{1,2,3,4,5,6,7,8}); // v = <1,2,3,4,5,6,7,8>;
配列の要素数がVector<T>.Countより多いと、多い分は無視される。以下だと1から8までが格納される。
var v = new Vector<int>(new[]{1,2,3,4,5,6,7,8,9,10}); // v = <1,2,3,4,5,6,7,8>;
index引数を指定すると配列のその位置から格納される。以下だと3から10までが格納される。巨大な配列を逐次処理するのに便利だな。
var v = new Vector<int>(new[]{1,2,3,4,5,6,7,8,9,10}, index:2); v = <3,4,5,6,7,8,9,10>;
配列の要素数がVector<T>.Countより少ないと、System.IndexOutOfRangeExceptionを吐く。 巨大な配列をインデックス指定でぶん回す場合は配列の要素数がVector<T>.Countで割り切れるようにパディングするといいな。
var v = new Vector<int>(new[]{1,2,3,4,5,6,7}); // System.IndexOutOfRangeException
使ってみる[編集 | ソースを編集]
基本は以下の繰り返し。
- 配列の一部分をVector<T>に格納する
- ベクトル演算を実行する
- 演算結果のVector<T>を配列に書き戻す
// 配列を用意する
var array = Enumerable.Range(0, 1024).ToArray();
var arraySize = array.Length;
// SIMDサイズ
var simdSize = Vector<int>.Count;
// インクリメント用
var one = Vector<int>.One;
// 実行
for (int index = 0; index < arraySize; index += simdSize)
{
// 配列をSIMDレジスタに格納
var vector = new Vector<int>(array, index);
// ベクトル演算Add
vector += one;
// SIMDレジスタを配列に書き戻す
vector.CopyTo(array, index);
}
ベンチマーク[編集 | ソースを編集]
BenchmarkDotNetで単純なインクリメントを比較してみた。
備考:比較対象のScalarOpはこんな感じ。
for (int index = 0; index < arraySize; index ++)
{
array[index] += 1;
}
結果。
BenchmarkDotNet=v0.12.1, OS=macOS Catalina 10.15.4 (19E287) [Darwin 19.4.0]
Intel Core i7-8700B CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.1.300
[Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
Job-AGKFKU : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
InvocationCount=1 UnrollFactor=1
| Method | ArraySize | Mean | Error | StdDev | Median |
|--------- |---------- |----------:|----------:|---------:|----------:|
| ScalarOp | 512 | 320.43 ns | 10.136 ns | 29.89 ns | 315.00 ns |
| VectorOp | 512 | 159.76 ns | 9.236 ns | 27.09 ns | 166.50 ns |
| ScalarOp | 1024 | 574.93 ns | 13.965 ns | 13.06 ns | 575.00 ns |
| VectorOp | 1024 | 169.69 ns | 9.599 ns | 28.00 ns | 170.50 ns |
加速されすぎ。