はじめに
結果は正しいけど、なぜか実行速度が極めて遅いコードがあって、アセンブリ見たら明らかに変な処理をしていたんだけれど、どうやらコンパイルオプションが悪かったせいだということがわかった。それはそれとして、コンパイラがなぜその「変な処理」を吐いたか、その気持ちを理解してみたい、という話。
現象
こんなコードを書く。
#include <immintrin.h>
extern __attribute__((aligned(64))) double z[1000000];
typedef double v8df __attribute__((vector_size(64)));
typedef double v4df __attribute__((vector_size(32)));
void
func(int i, v4df &zl, v4df &zh){
v8df zi = _mm512_load_pd((double*)(z + i));
zl = _mm512_extractf64x4_pd(zi,0);
zh = _mm512_extractf64x4_pd(zi,1);
}
要するに配列から512bitをごそっと持ってきて、その上位256bitと下位256bitをそれぞれ256bitレジスタに対応する変数に格納して返す、という処理。
これは、素直にアセンブリにするならこうなるだろう。
movslq %edi, %rdi
vmovups z(,%rdi,8), %zmm16
vextractf64x4 $0, %zmm16, %ymm0
vmovupd %ymm0, (%rsi)
vextractf64x4 $1, %zmm16, %ymm1
vmovupd %ymm1, (%rdx)
ret
もしくは、zmmの下位がymmであることを使ってこうしても良い。
movslq %edi, %rdi
vmovupd z(,%rdi,8), %zmm0
vmovapd %ymm0, (%rsi)
vextractf64x4 $0x1, %zmm0, (%rdx)
ret
しかし、Xeon(Haswell)上で、icpc -O3 -xHOST -S test.cpp
としてコンパイルするとこんなコードを吐く。
movslq %edi, %rdi
vmovups z(,%rdi,8), %zmm0
vmovups %zmm0, (%rsp)
vmovupd (%rsp), %ymm3
vmovupd 32(%rsp), %ymm4
vmovupd %ymm3, 64(%rsp)
vmovupd %ymm4, 96(%rsp)
vmovupd %ymm3, 128(%rsp)
vmovupd %ymm4, 160(%rsp)
vmovups 64(%rsp), %zmm1
vmovups 128(%rsp), %zmm5
vextractf64x4 $0, %zmm1, %ymm2
vextractf64x4 $1, %zmm5, %ymm6
vmovupd %ymm2, (%rsi)
vmovupd %ymm6, (%rdx)
vzeroupper
movq %rbp, %rsp
popq %rbp
ret
どうやら
- まず配列の中身をzmmに落とす
- zmmの中身をスタックに書き戻す
- zmmの上位256bitと下位256bitに対応するデータをymm3,4に読み込む
- ymmを使ってzmmの中身をスタックに個別にコピー
- メモリからzmm1とzmm5にデータを読み込む
- zmmの上位、下位256bitをymmにコピー
- 結果をメモリに書き戻す
ということをやっているらしい。
原因
これは、コンパイルオプションが悪いのが原因。vextractf64x4
はAVX-512なのに、その命令セットを実装していないHaswell上で-xHOST
をつけてコンパイルしたためにおかしくなった。ちゃんと、-xMIC-AVX51
をつけてコンパイルすれば所望のアセンブリを吐く。
なぜこうなったか?
ここまでが事実で、これからは「なぜこういうコードを吐いたか」かの推測(憶測)である。
コンパイルしたいのはこんなコードだった。
v8df zi = _mm512_load_pd((double*)(z + i));
zl = _mm512_extractf64x4_pd(zi,0);
zh = _mm512_extractf64x4_pd(zi,1);
コンパイラは、まず組み込み関数を機械的にアセンブリに変換してしまう。この時、レジスタ番号は仮のものを振る。
vmovups z(,%rdi,8), %zmmA
; (zmmB=zmmA)
; (zmmC=zmmA)
vextractf64x4 $0, %zmmB, %ymmD
vextractf64x4 $1, %zmmC, %ymmE
この時、プログラム的にはzmmBとzmmCはzmmAと同じ内容を指すことがわかっているから、そこをなんとかしないといけない。
最適化無しで、かつAVX-512に対応している場合(-O0 -xMIC-AVX512)、zmmBとzmmCへのコピーをメモリ経由でやる。
vmovups %zmmA, -440(%rbp)
vmovups -440(%rbp), %zmmB
vextractf64x4 $0, %zmmB, %ymmD
vmovups %zmmA, -344(%rbp)
vmovups -344(%rbp), %zmmC
vextractf64x4 $1, %zmmC, %ymmE
本当はもっとごちゃごちゃやってるけど、まぁエッセンスはこんなことをする。
最適化レベルを上げる(-O3 -xMIC-AVX512
)と、値のコピーをレジスタのコピーでやろうとする。
vmovups z(,%rdi,8), %zmmA
vmovaps zmmA, zmmB
vmovaps zmmA, zmmC
vextractf64x4 $0, %zmmB, %ymmD
vextractf64x4 $1, %zmmC, %ymmE
その後の最適化プロセスで、A,B,Cに同じレジスタ番号が振られる。
vmovups z(,%rdi,8), %zmm16
vmovaps zmm16, zmm16
vmovaps zmm16, zmm16
vextractf64x4 $0, %zmm16, %ymm0
vextractf64x4 $1, %zmm16, %ymm1
その後、無駄なvmovaps
が消えて完成。
vmovups z(,%rdi,8), %zmm16
vextractf64x4 $0, %zmm16, %ymm0
vextractf64x4 $1, %zmm16, %ymm1
さて問題は、レジスタzmmを持っていない命令セットを指定した場合である。Haswellマシンで-xHOST -O3
を指定した場合、-xCORE-AVX2
と解釈されているものと思われる。
まず、組み込み関数を機械的に置き換えるところまでは同じ。
vmovups z(,%rdi,8), %zmmA
; (zmmB=zmmA)
; (zmmC=zmmA)
vextractf64x4 $0, %zmmB, %ymmD
vextractf64x4 $1, %zmmC, %ymmE
さて、コンパイラは、zmmがどういうものかは知っているが、Haswellマシンで-xHOST
が指定されたため、自分が使って良いレジスタはymmまでだと思っている。この条件でzmmAの中身をzmmBやzmmCにコピーしないといけない。
この時、
- 既に出力されたzmmは使って良い
- しかし新たに使って良いレジスタはymmまで
- AVX-512の命令セットも使ってはならない
という条件がある。この条件でzmmBとzmmCにzmmAの中身をコピーするにはメモリ経由でやるしかない。
というわけで、冒頭に述べたようなymmを使ったメモリコピーのコードが吐かれたっぽい。
疑問
吐かれたコードを見ている限り、インテルコンパイラは-xCORE-AVX2
を指定した場合でも、組み込み関数で吐かれたzmm
をメモリに書き込む、メモリからzmm
へ読み込むコードは許しているように見える。
それなら、
vmovups z(,%rdi,8), %zmmA
vmovups %zmmA, (%rsp)
vmovups (%rsp), %zmmB
vmovups (%rsp), %zmmC
vextractf64x4 $0, %zmmB, %ymmD
vextractf64x4 $1, %zmmC, %ymmE
でいいじゃん、という気がするし、そもそも「新たにzmmを使ってはならない」「zmm間のコピーも許さない」という条件でも、いきなり
vmovups z(,%rdi,8), %zmmA
vextractf64x4 $0, %zmmA, %ymmD
vextractf64x4 $1, %zmmA, %ymmE
としてくれても良い気もする。他のコードのアセンブリを見ている限り、レジスタの値をスタックに積み、それを別のレジスタに読み込む、という処理があれば最適化で消えるので、組み込み関数を使った場合に最適化の振る舞いがおかしくなるのかなぁ、という気がした。
ちなみに
最適化レベルを最高にした場合、インテルコンパイラはvextractf64x4
を二個吐いたが、GCCは片方しか吐かず、zmm
の下位256bitがymm
であることを使っていた。
また、v8df
ではなく、組み込み型__m512d
を使って
#include <immintrin.h>
extern __attribute__((aligned(64))) double z[1000000];
typedef double v8df __attribute__((vector_size(64)));
typedef double v4df __attribute__((vector_size(32)));
void
func(int i, v4df &zl, v4df &zh){
__m512d zi = _mm512_load_pd((double*)(z + i));
zl = _mm512_extractf64x4_pd(zi,0);
zh = _mm512_extractf64x4_pd(zi,1);
}
とすると、-xCORE-AVX2
を指定しても所望のアセンブリを吐く。
movslq %edi, %rdi
vmovups z(,%rdi,8), %zmm0
vextractf64x4 $0, %zmm0, %ymm1
vextractf64x4 $1, %zmm0, %ymm2
vmovupd %ymm1, (%rsi)
vmovupd %ymm2, (%rdx)
vzeroupper
ret
謎。