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コンテナの型には使えないよ。