Arm SIMD intrinsic C++

ArmのSIMDをintrinsicでC++から叩きたい時のメモだよ。

#はじめに
この文章は書きかけだよ。
(多分永遠に書きかけだよ。)
書き換えだから書いてる途中で「いっけね間違ってたわ」となる可能性が高いよ。
だからあまり信用しないほうがいいと思うよ。

##名前は？
**S**calable **V**ector **E**xtenstion略して**SVE**って言うよ。~~最後の単語、二文字目をとってSVXにしなかったんだ…。~~

##コンパイラ & オプションは？
c++の場合はclangコンパチの``armclang++``を使うよ。
コンパイラオプションは、``-march=$(architecture name)+sve``だよ。後ろの``+sve``がSVE FLAGをonにするよ。
例えばArmv8.2なら
``-march=armv.8.2+sve``
だよ。
面倒な場合は
``-mcpu=native+sve``
とかでいいんじゃないかな。g++と違って``-march=native``じゃないところは注意。

##includeすべきものは？
```cpp
#ifdef __ARM_FEATURE_SVE
#include <arm_sve.h>
#endif
```
でいいよ。``__ARM_FEATURE_SVE``がSVEフラグだよ。

##型名は？
SVEに関連したものはたいてい``sv``から始まるよ。
例えば、倍精度(64bit)浮動小数点なら``svfloat64_t``だよ。
一般化すると``sv(type)(bit)_t``だよ。

##bit長は？
**気にしなくていいよ。**
何故ならArmがbit長依存なコードを書かせたくないから命令セットレベルではbit長を気にしなくていいようになっているよ。
すごいね。

##え？じゃあどうやってloop回すの？
気になるよね。
**面倒くさいぞ。**
まずはこういう事をしたいとしようか。

```cpp
const std::size_t N = 1024;
for(int i = 0 ; i < N ; ++ i){
    z[i] = x[i] + y[i];
}
```

これは1024要素の配列を2つ(``x, y``)取ってきて、それを足し込むよ(``z``)。
これをSVE化するよ。

###step1: プレディケート宣言
まずは**プレディケート**を宣言するよ。
プレディケートというとなんか仰々しいけど要するに``true``か``false``を返すなんかだよ。

```cpp
const uint64_t i = 0;
svbool_t pg = svwhilelt_b64(i, N);
```
頭の``sv``はいつもどおりSVEの意味で、``while``はwhile loopで用いられる事を意味しているよ(まあ別にwhileじゃなくてforを作るのに使ってもいいよ)。
``lt``はless thanで、大雑把に言うと``while(i < N)``的なloopを組むのにこれが使われるよ。
loopの先端は今回0だよ。
``_b64``は64bit変数である事を意味しているよ。

###step2: SVE型に配列をload
SVE用の``svfloat64_t``に``double``の配列をloadするよ。

```cpp
svfloat64_t x_sve = svld1(pg, &x[i]);
svfloat64_t y_sve = svld1(pg, &y[i]);
```
``svld1``の最初の引数にプレディケートを持ってくるよ。
第二引数にロードしたい値のポインタを持ってこよう。
これで、第二引数をbaseとして、SIMD幅分だけloadしてくるよ。
後ろにある``1``が気になるかもしれないけど、とりあえず今は気にしなくていいよ(実は``2``もあるよ今は使わないけど)。

###step3: 足し算する
当然普通に

```cpp
x_sve + y_sve
```
とか書いてもどうにもならないよ。
ちゃんと関数を使うんだよ。

```cpp
svfloat64_t z_sve = svadd_z(pg, x_sve, y_sve);
```
もうわかってると思うけど、``sv``はSVEで、``add``は加算だよ。
最後の``_z``は**多分**、SIMD幅から溢れた要素を0埋めてるんだと思うよ(zero-paddingのzかな？)。
(``_m``とか``_x``とかあるみたいなんだけどまだ試してないから何が起こるのかはよくわからないよ。)

###step4: 計算結果をSVE型からdouble型にstore
計算結果をSVE用の``svfloat64_t``から``double``にstoreするよ。

```cpp
svst1(pg, &z[i], z_sve);
```

###step5: loopカウンタを上げる
loopカウンタを上げるよ。
んな事言ったってSIMD幅がわからんのやったらどうにもならんやろがいと思うかもしれないけど、64bit変数が今SVE変数内に何個あるか教えてくれる関数``svcntd``があるよ。

```cpp
i += svcntd();
pg = svwhilelt_b64(i, N);
```
``cnt``はcountの略だよ。最後の``d``は多分doubleで、32bitなら``svcntw``(wordかな?)、16bitなら``svcnth``だよ。
あとiが変わったからプレディケートを作り直すのを忘れないようにしようね。

###step6: loopを止める
while loopしたら必ずloopをどこかで止めなきゃいけないよね。
これには``svptest_any``を使うよ。

```cpp
while(svptest_any(svptrue_b64(), pg));
```
``svptest``はプレディケートをテストするよ。
``any``は、SVE vector中の要素全てをチェックして、一つでも``false``なら``false``を返すよ。
他にも最初の要素を見る``first``とか最後の要素を見る``last``とかあった気がするけど一旦無視するよ。
``svptrue_b64()``は単に``true``だと思うよ。

##で？結局どうすればいいの？
こうだよ。

```cpp
int64_t i = 0;
svbool_t pg = svwhilelt_b64(i, N);
do{
    svfloat64_t x_sve = svld1(pg, &x[i]);
    svfloat64_t y_sve = svld1(pg, &y[i]);
    svfloat64_t z_sve = svadd_x(pg, x_sve, y_sve);
    svst1(pg, &z[i], z_sve);
    i += svcntd();
    pg = svwhilelt_b64(i, N);
}while(svptest_any(svptrue_b64(), pg));
```
##実行結果
2019年08月23日現在、実機でSVEをサポートしているものはないと思うよ。
というか知っている限り、Fugakuに導入されるCPUが世界初だよ。
従って、生で実行できる環境は存在しないよ。
エミュレータを使おうね。

#使用上の注意
その特性上、``svfloat64_t``などは、コンパイル時にバイトサイズが決まらず、実行時に決まるものだよ。
これをarmの専門用語で*sizeless*と言うよ。
sizelessなものにはいくつか使用上の制限があるよ。
というわけでarmのドキュメントに書いてあることの中で重要そうなのをほぼ丸写しするよ。

##やっていいこと
 - 自動ストレージ(要するに``static``や``extern``つきで宣言された変数ではない変数の事だよ)に使っていいよ。つまりベタ書きで

```cpp
svfloat64_t a;
```
とかできるよ。

 - 関数の引数や戻り値に使っていいよ。

```cpp
svfloat64_t hoge(svfloat64_t a){
    return a;
}
```
とかできるよ。

 - 複合リテラル``(type) {value}``に使っていいよ
ただいいって書いてあるんだけど実際に書いたらコンパイルエラーで落ちたよ。
armclangじゃなくてFujitsuのでコンパイルしたら通ったからまだ未対応なのかな。

 - C++の``type()``に使えるよ。
ただこれなんのことだかわからなかったよ。
キャストの事？誰かわかったら教えてほしいよ。

 - ポインタやリファレンスにしていいよ。

```cpp
svfloat64_t* ptr_a = &a;
svfloat64_t& ref_a = a;
```
とかできるよ。

##やっちゃだめなこと
 - ``static``や``thread_local``な変数には使えないよ。
 - SVE型の配列は作れないよ。
 - ``new``や``delete``は使えないよ。
 - ``sizeof``などの関数には放り込めないよ。
 - ポインタは作っていいけどポインタに対して演算は行えないよ。
 - 共用体``union``、構造体``struct``、クラス``class``とかはSVE型をメンバーに持てないよ。
 - SVE型を``throw``したり``catch``したりはできないよ。
 - ラムダ式は値そのものはキャプチャできないけど、リファレンスならキャプっていいよ。
 - STLコンテナの型には使えないよ。

#参考文献
https://qiita.com/aKatsuhiroMihara/items/f4799ae4e6f12743b88e#predicate-centric-approach%E3%81%A3%E3%81%A6%E3%81%AA%E3%82%93%E3%81%98%E3%82%83%E3%82%89%E3%81%BB%E3%81%84