ArmのSIMDをintrinsicでC++から叩きたい時のメモだよ。
はじめに
この文章は書きかけだよ。
(多分永遠に書きかけだよ。)
書き換えだから書いてる途中で「いっけね間違ってたわ」となる可能性が高いよ。
だからあまり信用しないほうがいいと思うよ。
名前は?
Scalable Vector Extenstion略して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すべきものは?
# 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回すの?
気になるよね。
面倒くさいぞ。
まずはこういう事をしたいとしようか。
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を返すなんかだよ。
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するよ。
svfloat64_t x_sve = svld1(pg, &x[i]);
svfloat64_t y_sve = svld1(pg, &y[i]);
svld1の最初の引数にプレディケートを持ってくるよ。
第二引数にロードしたい値のポインタを持ってこよう。
これで、第二引数をbaseとして、SIMD幅分だけloadしてくるよ。
後ろにある1が気になるかもしれないけど、とりあえず今は気にしなくていいよ(実は2もあるよ今は使わないけど)。
step3: 足し算する
当然普通に
x_sve + y_sve
とか書いてもどうにもならないよ。
ちゃんと関数を使うんだよ。
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するよ。
svst1(pg, &z[i], z_sve);
step5: loopカウンタを上げる
loopカウンタを上げるよ。
んな事言ったってSIMD幅がわからんのやったらどうにもならんやろがいと思うかもしれないけど、64bit変数が今SVE変数内に何個あるか教えてくれる関数svcntdがあるよ。
i += svcntd();
pg = svwhilelt_b64(i, N);
cntはcountの略だよ。最後のdは多分doubleで、32bitならsvcntw(wordかな?)、16bitならsvcnthだよ。
あとiが変わったからプレディケートを作り直すのを忘れないようにしようね。
step6: loopを止める
while loopしたら必ずloopをどこかで止めなきゃいけないよね。
これにはsvptest_anyを使うよ。
while(svptest_any(svptrue_b64(), pg));
svptestはプレディケートをテストするよ。
anyは、SVE vector中の要素全てをチェックして、一つでもfalseならfalseを返すよ。
他にも最初の要素を見るfirstとか最後の要素を見るlastとかあった気がするけど一旦無視するよ。
svptrue_b64()は単にtrueだと思うよ。
で?結局どうすればいいの?
こうだよ。
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つきで宣言された変数ではない変数の事だよ)に使っていいよ。つまりベタ書きで
svfloat64_t a;
とかできるよ。
- 関数の引数や戻り値に使っていいよ。
svfloat64_t hoge(svfloat64_t a){
return a;
}
とかできるよ。
-
複合リテラル
(type) {value}に使っていいよ
ただいいって書いてあるんだけど実際に書いたらコンパイルエラーで落ちたよ。
armclangじゃなくてFujitsuのでコンパイルしたら通ったからまだ未対応なのかな。 -
C++の
type()に使えるよ。
ただこれなんのことだかわからなかったよ。
キャストの事?誰かわかったら教えてほしいよ。 -
ポインタやリファレンスにしていいよ。
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コンテナの型には使えないよ。
参考文献