ARM SVE というベクタ拡張があるんだけど、それって公開されてるコンパイラでどのぐらい対応されるんや?という話題を見かけたので、ちょっと試してみたら良い感じだったのでついでにもう少し調べてみた。
http://d.hatena.ne.jp/w_o/20150423#1429775436
個人的には自動ベクタライザというのは信用してないんだけど、それはそれとして、ループ変換ツールとしての最適化パスの処理はまあ面白いと思ってるので。
GCCは、ビルドしてもいいけど、
https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads
ここからビルド済みバイナリが取得できるので、そちらのほうが簡単だと思う。
このGCCに、-march=armv8-a+sve -O3 を渡すと、SVEを使って自動ベクタライズしてくれるようになる。
自動ベクタライザを信用しない理由として、上のリンク先で上げてる通り、どうしても命令数がかなり膨らんでしまうというのがありますね。これは性能上のデメリットもあるんだけど、出るasmの見た目が汚なくなるから嫌という気持ちの問題も大きい。
ところが、SVE向けに自動ベクトル化で出したコードは、この命令数の膨張が無くて、見ためがスッキリしてたので好きになった。
int f(int *__restrict__ a,
int *__restrict__ b,
int *__restrict__ c,
int n)
{
for (int i=0; i<n; i++) {
a[i] += b[i] * c[i];
}
}
これが、
f:
.LFB0:
.cfi_startproc
cmp w3, 0
ble .L2
mov x4, 0
sxtw x3, w3
ptrue p1.s, all
whilelo p0.s, xzr, x3
.p2align 3
.L3:
ld1w z1.s, p0/z, [x0, x4, lsl 2]
ld1w z2.s, p0/z, [x1, x4, lsl 2]
ld1w z0.s, p0/z, [x2, x4, lsl 2]
mad z0.s, p1/m, z2.s, z1.s
st1w z0.s, p0, [x0, x4, lsl 2]
incw x4
whilelo p0.s, x4, x3
bne .L3
.L2:
ret
vectorizeしない場合は、
f:
.LFB0:
.cfi_startproc
cmp w3, 0
ble .L2
mov x4, 0
.p2align 3
.L3:
ldr w6, [x0, x4, lsl 2]
ldr w5, [x1, x4, lsl 2]
ldr w7, [x2, x4, lsl 2]
madd w5, w5, w7, w6
str w5, [x0, x4, lsl 2]
add x4, x4, 1
cmp w3, w4
bgt .L3
.L2:
ret
ループ前の処理が3命令増えているが、ループ中の命令数は全く同じで、関数レベルで見ても12->15 の 3命令しか増えていない。neonの場合は、これが47命令の+35命令になって、ここに貼るのも嫌なレベルだ。
一番大きいのは、ループ回数がSIMD幅で割り切れない時の端数の処理に必要なマスクをwhilelo 命令一発で作れるところだろう。
http://d.hatena.ne.jp/w_o/20160901#1472719309 に昔説明を書いたけど、実際これだけ自動ベクタライザがスッキリした命令出すのを見てかなり感動しましたね。
自動ベクタライザが出すasmは汚いという時代はもう終わったんだ。
あと
int f(int *__restrict__ a,
int *__restrict__ b,
int *__restrict__ c,
int n)
{
for (int i=0; i<n; i++) {
if (a[i]) {
a[i] += b[i] * c[i];
}
}
}
こういう条件付きストアがある場合でも
f:
.LFB0:
.cfi_startproc
cmp w3, 0
ble .L2
mov x4, 0
sxtw x3, w3
ptrue p2.s, all
whilelo p1.s, xzr, x3
.p2align 3
.L3:
ld1w z1.s, p1/z, [x0, x4, lsl 2]
cmpne p0.s, p2/z, z1.s,
and p0.b, p0/z, p0.b, p1.b
ld1w z0.s, p0/z, [x1, x4, lsl 2]
ld1w z2.s, p0/z, [x2, x4, lsl 2]
mad z0.s, p2/m, z2.s, z1.s
st1w z0.s, p0, [x0, x4, lsl 2]
incw x4
whilelo p1.s, x4, x3
bne .L3
.L2:
ret
スッキリしていて美しいですね。(でも条件付きストアはよく考えたらAVX512でも同じことになるのでSVE固有ではない)
で、まあそれはよくて、この whilelo みたいなほぼアーキ固有みたいな命令をGCCがどう扱ってるのか気になったので調べたくなった。
まあこれが富士通コンパイラなら、なんか色々追加したんだろうな、という気持ちになるけど、GCCだとあんまりアーキ固有ベッタリな変換パスがベクタライザに入ってるイメージがないので。
調べた結果としては、以下のとおりだった。
自分で調べる人は、 gcc-8.3で、-fdump-tree-all して、xx.c.161t.vect とかを見れば、色々調べられると思う。あとは -fopt-info-vec-missed の出力も参考になるかもしれない。
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/internal-fn.def;h=4080e1698ea82404a1102ef586cce64100033e76;hb=4ac50a4913ed81cc83a8baf865e49a2c62a5fe5d#l136
WHILE_ULT という whilelo と対応するtree nodeが生まれている。これはまんまwhileloで、ループ最大値とループカウンタを受け取って、それをもとにマスクを作る。
あとMASK_LOAD,MASK_STOREもある。
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/internal-fn.def;h=4080e1698ea82404a1102ef586cce64100033e76;hb=4ac50a4913ed81cc83a8baf865e49a2c62a5fe5d#l118
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/internal-fn.def;h=4080e1698ea82404a1102ef586cce64100033e76;hb=4ac50a4913ed81cc83a8baf865e49a2c62a5fe5d#l131
AVX512 用に自動ベクトル化した場合もMASK_LOAD、MASK_STOREを使うようだ。
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/tree-vect-loop.c;h=c74a485cc2f638c26498ac6edb57faa221f5a46d;hb=4ac50a4913ed81cc83a8baf865e49a2c62a5fe5d#l1290
direct_internal_fn_supported_p で、バックエンドがWHILE_ULTをサポートしてるかどうか見てる。
https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=gcc/config/aarch64/aarch64-sve.md;h=d88553405094853ab58ba1c55cd212092a14116c;hb=4ac50a4913ed81cc83a8baf865e49a2c62a5fe5d#l1254
direct_internal_fn_supported_p は多分このmdから生成したテーブルとかで見てるんではないかな…(未確認)
それで、LOOP_VINFO_FULLY_MASKED_P が決定されてその場合は端数処理を付けないようにしてると思う。
ベクタライズのアルゴリズム自体は…みんなの宿題だゾ!