VOOZH about

URL: https://qiita.com/kaityo256/items/7da336428b81d8d8bf3b

⇱ インテルコンパイラで対応していない命令セットの組み込み関数を使った場合のアセンブリ #C++ - Qiita


👁 Image
10

Go to list of users who liked

5

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

More than 5 years have passed since last update.

@kaityo256(ロボ太)

インテルコンパイラで対応していない命令セットの組み込み関数を使った場合のアセンブリ

10
Last updated at Posted at 2017-05-17

はじめに

結果は正しいけど、なぜか実行速度が極めて遅いコードがあって、アセンブリ見たら明らかに変な処理をしていたんだけれど、どうやらコンパイルオプションが悪かったせいだということがわかった。それはそれとして、コンパイラがなぜその「変な処理」を吐いたか、その気持ちを理解してみたい、という話。

現象

こんなコードを書く。

test.cpp
# 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

どうやら

  1. まず配列の中身をzmmに落とす
  2. zmmの中身をスタックに書き戻す
  3. zmmの上位256bitと下位256bitに対応するデータをymm3,4に読み込む
  4. ymmを使ってzmmの中身をスタックに個別にコピー
  5. メモリからzmm1とzmm5にデータを読み込む
  6. zmmの上位、下位256bitをymmにコピー
  7. 結果をメモリに書き戻す

ということをやっているらしい。

原因

これは、コンパイルオプションが悪いのが原因。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にコピーしないといけない。

この時、

  1. 既に出力されたzmmは使って良い
  2. しかし新たに使って良いレジスタはymmまで
  3. 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を使って

test2.cpp
# 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を指定しても所望のアセンブリを吐く。

test2.s
 movslq %edi, %rdi
 vmovups z(,%rdi,8), %zmm0
 vextractf64x4 $0, %zmm0, %ymm1
 vextractf64x4 $1, %zmm0, %ymm2
 vmovupd %ymm1, (%rsi)
 vmovupd %ymm2, (%rdx) 
 vzeroupper 
 ret 

謎。

10

Go to list of users who liked

5
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10

Go to list of users who liked

5