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


# はじめに

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

# 現象

こんなコードを書く。

```cpp: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レジスタに対応する変数に格納して返す、という処理。

これは、素直にアセンブリにするならこうなるだろう。

```nasm
        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であることを使ってこうしても良い。

```nasm
        movslq  %edi, %rdi
        vmovupd z(,%rdi,8), %zmm0
        vmovapd %ymm0, (%rsi)
        vextractf64x4   $0x1, %zmm0, (%rdx)
        ret
```

しかし、Xeon(Haswell)上で、`icpc -O3 -xHOST -S test.cpp` としてコンパイルするとこんなコードを吐く。

 ```nasm
        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`をつけてコンパイルすれば所望のアセンブリを吐く。

# なぜこうなったか？

ここまでが事実で、これからは「なぜこういうコードを吐いたか」かの推測(憶測)である。

コンパイルしたいのはこんなコードだった。

```cpp
  v8df zi = _mm512_load_pd((double*)(z + i));
  zl = _mm512_extractf64x4_pd(zi,0);
  zh = _mm512_extractf64x4_pd(zi,1);
```

コンパイラは、まず組み込み関数を機械的にアセンブリに変換してしまう。この時、レジスタ番号は仮のものを振る。

```nasm
        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へのコピーをメモリ経由でやる。

```nasm
        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`)と、値のコピーをレジスタのコピーでやろうとする。

```nasm
        vmovups   z(,%rdi,8), %zmmA
        vmovaps   zmmA, zmmB
        vmovaps   zmmA, zmmC
        vextractf64x4 $0, %zmmB, %ymmD
        vextractf64x4 $1, %zmmC, %ymmE
```

その後の最適化プロセスで、A,B,Cに同じレジスタ番号が振られる。

```nasm
        vmovups   z(,%rdi,8), %zmm16
        vmovaps   zmm16, zmm16
        vmovaps   zmm16, zmm16
        vextractf64x4 $0, %zmm16, %ymm0
        vextractf64x4 $1, %zmm16, %ymm1
```

その後、無駄な`vmovaps`が消えて完成。

```nasm
        vmovups   z(,%rdi,8), %zmm16
        vextractf64x4 $0, %zmm16, %ymm0
        vextractf64x4 $1, %zmm16, %ymm1
```

さて問題は、レジスタzmmを持っていない命令セットを指定した場合である。Haswellマシンで`-xHOST -O3`を指定した場合、`-xCORE-AVX2 `と解釈されているものと思われる。

まず、組み込み関数を機械的に置き換えるところまでは同じ。

```nasm
        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`へ読み込むコードは許しているように見える。

それなら、

```nasm
        vmovups   z(,%rdi,8), %zmmA
        vmovups   %zmmA, (%rsp)
        vmovups   (%rsp), %zmmB
        vmovups   (%rsp), %zmmC
        vextractf64x4 $0, %zmmB, %ymmD
        vextractf64x4 $1, %zmmC, %ymmE
```

でいいじゃん、という気がするし、そもそも「新たにzmmを使ってはならない」「zmm間のコピーも許さない」という条件でも、いきなり

```nasm
        vmovups   z(,%rdi,8), %zmmA
        vextractf64x4 $0, %zmmA, %ymmD
        vextractf64x4 $1, %zmmA, %ymmE
```

としてくれても良い気もする。他のコードのアセンブリを見ている限り、レジスタの値をスタックに積み、それを別のレジスタに読み込む、という処理があれば最適化で消えるので、組み込み関数を使った場合に最適化の振る舞いがおかしくなるのかなぁ、という気がした。

# ちなみに

最適化レベルを最高にした場合、インテルコンパイラは`vextractf64x4`を二個吐いたが、GCCは片方しか吐かず、`zmm`の下位256bitが`ymm`であることを使っていた。

また、`v8df`ではなく、組み込み型`__m512d`を使って

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

```nasm: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     
```

謎。