2023年7月、Intel から新たな命令拡張、APX(Advanced Performance Extension) と AVX10 が発表された。
これまでのx86拡張命令はSIMD命令を中心として、並列性のある処理を効率よく扱うものがメインだったのに対して、APX では、スカラ整数命令、つまり、普段よく使う命令に
- 汎用レジスタ数が倍増 (16→32)
- 3 operand の採用
という大きな拡張が入っている。これの解説を書いていく。
APX
REX2 prefixというprefixが増えている(また!prefixが増えた!)
REX2 prefixは、0xD5に割り当てられている。
0xD5 は 32bit までは AAD という命令に割り当てられていた。
x86を使っている人なら当然知っているので説明不要と思うが、32bit までの x86 には、BCD を扱うための専用命令がいくつかあって、それがオペコードマップを無駄に埋めていた。これらの命令は、x86-64 CPU では使われておらず、実行すると#UD(未定義)例外を吐いていた。
REX2 prefix は、x86-64 だけサポートすればいいので、この0xD5 をprefix として使っている。
既存の REX prefixは、0x4? という1byteで、上位4bitが0x40 、下位4bit で、WRXBという4つのビットをエンコードしている。(昔書いた : https://w0.hatenablog.com/entry/20130126/1359183872)
R,X,Bはそれぞれ、レジスタのインデクスに対応していて、これを使うことで、命令にレジスタのインデクスを1bit拡張して、レジスタ数を16本に増やしていた。Wは、演算が64bitか32bitかを示している。
これまでの REX prefix
add r13, r15 : 4d 01 fd
r/m = r13
reg = r15
4d : REX prefix の 0x40 | w=1, R=1, X=0, B=1 (Xは未使用)
01 : add rm, reg
fd : mod=11, reg=111, r/m=101
r13 = Bの1 | r/m の 101 で 1101 = r13
r15 = Rの1 | reg の 111 で 1111 = r15
REX2 は先頭1byteが0xD5 で、次に続く1byte に、M0,R4,X4,B4,W,R3,X3,B3 という8bitをエンコードしている。W,R3,X3,B3 は、REX prefixの WRXB と対応している。REX2では、これにさらにR4,X4,B4が付いて、レジスタのインデクスが5bit表現できるようになっている。
REX2 prefix (確認方法ないのでまちがってたらゴメンネ)
add r29, r31 : d5 5d 01 fd
r/m = r29
reg = r31
d5 : REX2 prefix
5d : M0=0, R4=1, X4=0, B4=1, W=1, R3=1, X3=0, B3=1 (X4, X3 は未使用)
01 : add rm, reg
fd : mod=11, reg=111, r/m=101
r29 = B4の1 | B3の1 | r/m の 101 で 11101 = r29
r31 = R4の1 | R3の1 | reg の 111 で 11111 = r31
残ったM0
ビットは、命令のマップを示している。
x86 では、昔は使用頻度の低かった命令や、あとから拡張された命令は、0x0f というプレフィクスが付いていた。たとえば、cmov
命令などは、0x0f プレフィクスが付いている
cmove eax, eax # 0f 44 c0
REX2 の M0
ビットは、この 0x0f プレフィクスが付いているかどうかを示している。0x0f プレフィクスが付かない命令では、REX2 は REX初代より1byte増えてしまっているが、REX2 では 0x0f プレフィクスは1bit にエンコードされているので長さが変わらない。バイト数は変わらず、指定できるレジスタ数が倍になっている。
cmove r15, r15 # 4d 0f 44 ff # with REX prefix
cmove r31, r31 # d5 ff 44 ff # with REX2 prefix
このREX2 prefixによって、レジスタ数を倍増している。
さらに、APXでは、EVEX prefix を整数スカラ命令にも付けられるようになっている。EVEX prefix は、3-operand 命令を表現できるので、3-operand 命令を使うときは、整数スカラ命令にもEVEX prefixを付ける。
AVX-512 では、EVEX プレフィクスを付けて、それに続く3byteに情報をエンコードしていた。
(ベクタ用に使うEVEX (AVX-512で使ってるやつ))
これをスカラ整数命令にも使えるように以下のように拡張している
(整数スカラ用に使うEVEX (APXで使うやつ))
- mmmm bit の上位2bitは 0 に reserved されていたが、この1bitは別のB4に使われている。残りの1bitが1になっているとスカラ整数命令になる
- 最上位1bitはB4に割り当てられている。AVX-512ではアドレスレジスタが16本しか無かったので、アドレスとして使われる整数レジスタを示すX,Bは4bitでよかったがAPXによって整数レジスタも32本になったので、それを表現できるようにX,Bも拡張されている
- aaa bit (マスクレジスタ指定) は整数スカラ命令では使わないようになっていて、かわりにNF-bit が入っている
- b bit (broadcast, 丸め指定) は整数スカラ命令では使わないようになっていて、かわりにND-bit が入っている
- pp bit (prefix指定) は、SIMDでは、ほぼオペコードの一部になっていた prefix をエンコードしていたが、スカラ整数命令では0x66 prefixに意味があるので、スカラ整数命令では解釈が変わっている(後述)
このEVEX を、"Extended EVEX prefix = Extended Enhanced Vector Extension prefix!! = 拡張された強化されたベクタ拡張 prefix!!!!" と呼ぶと書いてある。
ND-bit が立っていると、3-operand が有効になる。VVVVV で指定されるレジスタが3個目のオペランドになる。
NF-bit が立っていると、フラグ(EFLAGS)が更新されず、前回の値が保存されるようになる。
拡張された命令は、オペコードの前に 0x0f, 0x0f38, 0x0f3a のどれかが付いて、それによって命令が識別されていたが、APX拡張では、0x0f, 0x0f38, 0x0f3a が、それぞれ opcode map を持っているというように理解が整理されている。
Extended EVEX の mmmm は、意味こそもとのEVEXとほぼ変わっていないが、この mmmm は map indexを示す値だというように再解釈されていて、
- mmmm = 100 : legacy map 0
- mmmm = 101 : legacy map 1 # 0x0f が付いていた命令
- mmmm = 110 : legacy map 2 # 0x0f38 が付いていた命令
- mmmm = 111 : legacy map 3 # 0x0f3a が付いていた命令
というように表現されている。
pp bit は AVX-512 の pp と似ているが、0x66, 0xf2, 0xf3 がほぼオペコードの一部になっていたSIMD命令と違って、整数スカラ命令では0x66 prefixは厳密にオペランドサイズを変えるprefixだと決まっている。
SIMD 命令では、0x66, 0xf2, 0xf3 は排他的にしか使われなかったので、選択式で問題なかった。
しかし、APXでは、CRC32のように、オペコードの一部として0xf2プレフィクスを使いながら、オペランドサイズが変わる命令があり、0xf2と0x66が同時に使われることがある。
crc32 eax, ax # 66 f2 0f 38 f1 c0 # 66 prefix と f2 prefix を同時に含む
このような場合は…いまいち理解できず…
おそらく、曖昧性がなく整合性とれるように、オペコードを書きかえられたりしてるのだと思う。
CRC32の場合は、0xf2 prefixがなくても別の命令と衝突しないので、0xf2 prefix が消されている。"PP"の列の括弧でくくられているのは元の命令と割り当てられているバイトが変わってるという意味で、括弧内が、オリジナルx86でのプレフィクス。
https://www.felixcloutier.com/x86/lzcnt lzcnt とかはオリジナルx86命令では 0xf3 prefixがオペコードの一部になっていて、さらに0x66 prefixを付けてoperand size を変えられるが、0xf3 prefix が無くなって、オペコードが 0xBD から 0xF5に変わっている、(と書いてあるはず…理解不足)
0xf2, 0xf3 は、本来は movs などの string 命令に付けるrep prefixであるが、extended EVEX は string 命令には付かないので、rep prefixをEVEX prefix内にエンコードする方法はないようだ。
eflagsに関わる命令の強化
条件分岐にかかわる、cmp, test 命令、条件付きmovをするcmov,setccに大幅な強化が入っている。
- SCC (Source Condition Code)
cmp, test 命令に SCC という4つのビットが付けられるようになっている。付けられる命令は、ccmp, ctest という名前になっている。
SCCの各ビットはx86命令の16種類のcondition codeと対応している。 https://www.sandpile.org/x86/cc.htm
ccmp, ctest 命令は、このSCCビットを使ってそもそも比較を行うかどうかを指定できる。
if ((x!=4) && (y!=4)) {
aaa();
}
こういうコードを考える。
既存のx86では、flags 同士の AND は取れないので、cmp するごとに分岐する必要がある。
cmp edi, 4
jne not4
cmp esi, 4
jne not4
call aaa
not4:
...
ccmp があれば、この分岐を一個に減らすことができる
cmp edi, 4
# eflags の ne を見て、cmp を実行するかどうかを決める
# ne が成立しない場合(Zが立ってる場合)は、eflags が dfv で指定された値になる(書式不明)
ccmpne esi, 4, dfv
jne not4
call aaa
not4:
...
これによって、分岐の数がいくらか減らせるようになると思う。(結構インパクトあるぐらい減らせるんじゃないかな…)
分岐予測が外れるような場合はもちろん、実行した分岐命令の数は、分岐予測のリソースを使うので、分岐予測が当たる場合でも、分岐命令は少ないほうがいい。
- zu
x86 でつらい点として、setcc は、 8bit レジスタしかオペランドに取れないというつらい点があった。
int x = 0;
if (flag)
x = 1
としたとき、setccがレジスタの下8bitしか書きかえてくれないので、32bit の 1 を作ろうとすると、事前に上位をクリアしておく必要があった
xor eax, eax # setcc が al しか書きかえてくれないので上位クリアがいる
test edi, edi
setne al
ret
これは、命令が無駄な上に、partial register 書きかえなので、いらない依存が増えてて最悪である。
ZU を付けると、ちゃんと上位ビットもゼロクリアしてくれるようになる。
- cfcmov (Cnditionally Faulting cmov)
cfcmov という命令が追加された。動作は、cmov と同じだが、condition が当たらなかった側のメモリのページフォルトを発生させないという動作になっている。
int x = 4;
if (p) {
x = *p;
}
このようなコードを考える。これは、cmov におきかえることはできない。
mov rax, 4
mov rcx, p
test rcx, rcx # p != 0
cmovnz rax, [rcx] # rcx が zero でないならロード
これは、元のCプログラムを維持していない。なぜなら、[rcx] の値は使っていないが、ロードしてしまっているため、アドレスゼロにアクセスして、ページフォルトが出てしまうからだ。
mov rax, 4
mov rcx, p
test rcx, rcx # p != 0
jz 1f
mov rax, [rcx] # rcx が zero でないならロード
1:
..
ちゃんとこう書きましょう。
cfcmov があれば、条件成立しない場合はページフォルト起こらないので、
mov rax, 4
mov rcx, p
test rcx, rcx # p != 0
cfcmovnz rax, [rcx] # rcx が zero でないならロード
これでいけるね〜
PUSH/POPの強化
aarch64 の ldp, stp みたいなのが増えた。2個のレジスタを同時にpush,popするpush2, pop2 という命令ができている。
さらに、push, pop をペアで使う時用のヒントが付けられるようになった。
push, pop のペアが一致していると、ストアバッファも経由しないで、直接レジスタリネームだけでレジスタ間の移動が実現できる(というようなことが書いてある)
AVX10
AVX10 は…(まだよく見てない(すいません))
AVX-512 ではアーキテクチャごとに対応する命令が違い、どのCPUがどの命令に対応しているか把握が難しいという状態だったが(gcc に -mavx512 オプションを渡す度に -mavx512 オプションがなくてキレそうになる)これを整理して、これからは「新しい拡張は以前の拡張を含む」というように誓ったのが重要…だと思う。"AVX10" という名前は、「今後はAVX11, AVX12というように順に増やしていきます」という気持ちの表れではないかと思う。(AVX512ER、AVX512DQ、AVX512CD…とかの違いを覚えなくてよくなる)
https://fuse.wikichip.org/news/3099/centaur-unveils-its-new-server-class-x86-core-cns-adds-avx-512/2/ より
違った。AVX10 は、 AVX10.1, AVX10.2, AVX10.X というように増やしていくっぽい。CPUID を見ると、"AVX10 Version N" の、"N" の部分がとれるようになっている。
https://cdrdv2.intel.com/v1/dl/getContent/784267 より
AVX-512では、feature ごとに bit が立っていたが、AVX10 では、8bit 整数(EAX=24H,ECX=00H:EBX[bit 7:0])が付いてるだけで、AVX10.N という拡張しか許されないようになっている(feature bit はもうやりませんというIntelの誓い)
そんなことなかった。"Reserved for discrete feature bits" があるやん…つまり…つまりどういうことなの…(放棄)
Vector Length だけが、Optional になっていて、CPUによって対応するSIMD長が128bit, 256bit, 512bitと変わるようになっている。
初期の AlderLake では、P-Core が512bit命令サポート、E-Core が256bit命令サポートとなっていたが、現代のソフトウェア環境では、命令セットの違うふたつのCPUを混ぜるのは、あまり対応されておらず、P-core の512bit命令は無効化された状態で販売されていた(その後回路が消されたらしい)。
これは、P-core の演算器が無駄になっている。
また、AVX-512 命令は、3-operand、独立したマスクレジスタ、丸めモードの指定、スケールの拡張など、256bit演算に使う場合でも、有用な機能が含まれていた。512bit対応しない場合でもAVX-512で採用されたEVEXエンコーディングは有効な場面があった。
これが整理されて、AVX10では、512bit対応をオプショナルとすることで、
- サーバー向けXeon は、512bit命令対応
- デスクトップ向け i7 は、電力効率を上げるために256bit命令のみ対応。AVX、AVX2 と違いレジスタ数が32個に増え、マスクレジスタや3-operandが使えるようになっている
- (省電力向けCeleron(?) は、128bit命令のみ対応?)
というような区分けができるようになった