xsave
AVXとレジスタのコンテキスト
AVX登場より前は、コンテキストスイッチのFPUレジスタ保存時に保存するレジスタセットにあわせて命令を駆使していた。
最終的にFPU,MMX,SSEレジスタ保存時には、fxsave/fxrstor という命令を使うようになっている(と思う)。
fxsave/fxrstor は、固定フォーマットで FPU, MMX, SSE の状態を保存、復元する。
(Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture より)
fxsave/fxrstorまでは、レジスタの数やサイズが変わるごとに命令を追加していた。これだと、命令拡張するごとにOSのコンテキスト保存処理の更新が必要になる。
実際、fxsaveはAVXに対応していないし、さらにその上にAVX-512が乗ると、さらに命令追加しないといけない。さらに言うと、現時点で既にAMXというさらに追加の拡張が入ることも決まっている。
このへんの問題を整理し、FPUレジスタの保存、復元を担う命令が、xsave/xrstor 等の命令群である。
xsave/xrstor では、格納するコンテキストのサイズを実行時にCPUIDから取得できるようになり、そのサイズを確保してxsaveすれば、対応するコンテキストが一括で保存されるようになった。一応、形としては、xsave命令だけ対応すれば、将来レジスタセットが大きくなった場合にも対応できるようになっている。
(実際には、そんなにうまくいかなくて、AMXが付いたCPUではコンテキストサイズが10KiBと巨大になっており、そのまま単純に扱うのは難しくなっている)
以下、この xsave 命令について説明していく。
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html 基本的には、volume 1 の CHAPTER 13 "MANAGING STATE USING THE XSAVE FEATURE SET" を一次資料として読むのがよいと思う。(Vol3 にもOS対応についてちょっと書いてある)
state-component bitmaps
xsave を理解する上で、まず知っておくべきは、state-component bitmaps と呼ばれる状態を示す64bitのビット列だ。
state-component bitmaps は、xsaveに関連する各種操作において、操作する状態を指示するのに使う。64bit のうちの、各ビットが、
bit | レジスタ |
---|---|
0 | x87 (+MMX) |
1 | SSE |
2 | AVX |
3 | 今は亡きMPX |
4 | 今は亡きMPX |
5 | AVX-512 opmask (k0-k7のこと) |
6 | AVX-512 ZMM0-15 の上位 |
7 | AVX-512 ZMM16-31 |
8 | PT (Processor Trace の何か) |
9 | PKRU (知らん) |
11 | CET |
12 | CET |
13 | HDC (知らん) |
16 | HWp (知らん) |
17 | AMX の XTILECFG |
18 | AMX の XTILEDATA |
他 | reserved |
というように、レジスタセットと対応している。
例えば、xsave 命令は、保存すべきレジスタセットをオペランドに指定できるが、このとき "0x5 (0b101)" を指定すると、(CPUが対応していれば) x87 と AVX の状態を保存するようになる。
現在使えるレジスタセットの一覧も、このstate-component bitmaps で表現される。
OS が対応していて、使えるレジスタの一覧は、XCR0 と呼ばれるレジスタに保存されている。XCR0は、ユーザー空間からも読めて、_xgetbv intrinsics で取得できる。
#include <stdio.h>
#include <immintrin.h>
int main()
{
auto xcr0 = _xgetbv(0);
printf("XCR0 : %016llx\n", (long long)xcr0);
}
手元の環境だと、 XCR0の値は、0x207 となっていた。これは、state-component bitmaps になっていて、PKRU + AVX + SSE + X87FPU がこのプログラムから使えるレジスタセットだということになる。
xsave と cpuid
xsave がサポートされているかどうかは cpuid の 1番目のECXの26bit目を見ればよい。
# cpuid コマンドの結果 (XSAVE/XSTOR states のところを見よう)
feature information (1/ecx):
PNI/SSE3: Prescott New Instructions = true
PCLMULDQ instruction = true
DTES64: 64-bit debug store = true
MONITOR/MWAIT = true
CPL-qualified debug store = true
VMX: virtual machine extensions = true
SMX: safer mode extensions = true
Enhanced Intel SpeedStep Technology = true
TM2: thermal monitor 2 = true
SSSE3 extensions = true
context ID: adaptive or shared L1 data = false
SDBG: IA32_DEBUG_INTERFACE = true
FMA instruction = true
CMPXCHG16B instruction = true
xTPR disable = true
PDCM: perfmon and debug = true
PCID: process context identifiers = true
DCA: direct cache access = false
SSE4.1 extensions = true
SSE4.2 extensions = true
x2APIC: extended xAPIC support = true
MOVBE instruction = true
POPCNT instruction = true
time stamp counter deadline = true
AES instruction = true
XSAVE/XSTOR states = true
OS-enabled XSAVE/XSTOR = true
AVX: advanced vector extensions = true
F16C half-precision convert instruction = true
RDRAND instruction = true
hypervisor guest status = false
さらに、eax=0xd,ecx=0x0 または eax=0xd,ecx=0x1 で cpuid を実行すると、XSAVEの機能の詳細を取得できる。
$ cpuid -1 -l 0xd -s 0 # eax=0xd, ecx=0x0
CPU:
XSAVE features (0xd/0):
XCR0 valid bit field mask = 0x0000000000000207
XCR0 supported: x87 state = true
XCR0 supported: SSE state = true
XCR0 supported: AVX state = true
XCR0 supported: MPX BNDREGS = false
XCR0 supported: MPX BNDCSR = false
XCR0 supported: AVX-512 opmask = false
XCR0 supported: AVX-512 ZMM_Hi256 = false
XCR0 supported: AVX-512 Hi16_ZMM = false
IA32_XSS supported: PT state = false
XCR0 supported: PKRU state = true
XCR0 supported: CET_U state = false
XCR0 supported: CET_S state = false
IA32_XSS supported: HDC state = false
IA32_XSS supported: UINTR state = false
LBR supported = false
IA32_XSS supported: HWP state = false
XTILECFG supported = false
XTILEDATA supported = false
bytes required by fields in XCR0 = 0x00000a88 (2696)
bytes required by XSAVE/XRSTOR area = 0x00000a88 (2696)
$ cpuid -1 -l 0xd -s 1 # eax=0xd, ecx=0x1
CPU:
XSAVE features (0xd/1):
XSAVEOPT instruction = true
XSAVEC instruction = true
XGETBV instruction = true
XSAVES/XRSTORS instructions = true
XFD: extended feature disable supported = false
SAVE area size in bytes = 0x00000670 (1648)
IA32_XSS lower 32 bits valid bit field mask = 0x00019900
IA32_XSS upper 32 bits valid bit field mask = 0x00000000
基本的には、これで取得できる
bytes required by fields in XCR0 = 0x00000a88 (2696)
この領域を確保して、xsave/xrstor すればxsave対応はできる。
(xsaveが使えれば十分という人(どんな人や?)はここで読み終えてもらって構わない)
xsave で保存された状態の詳細を知りたい場合や、あとで説明するxsavec を使う場合は、さらにCPUIDを使って詳細を取得する。
CPUID に eax=0xd, ecx=state-component bitmap のビット位置を渡すと、ecxで指定されたビットと対応する状態の詳細が取れる。例えば、ecx=2 とすれば、AVX レジスタの情報を取得できる。 (x87 FPU と SSE のレジスタの情報は固定なので、これはCPUIDではなく各自がマニュアルから静的に取得する)
$ cpuid -1 -l 0xd -s 2
CPU:
AVX/YMM features (0xd/2):
AVX/YMM save state byte size = 0x00000100 (256)
AVX/YMM save state byte offset = 0x00000240 (576)
supported in IA32_XSS or XCR0 = XCR0 (user state)
64-byte alignment in compacted XSAVE = false
XFD faulting supported = false
byte offset は xsave でメモリ上に保存された状態のどこに保存されているかを示す。byte size は、この状態のサイズを示す。alignement については、xsavecのところで説明する。XFDは…まだ調べてないのでよく知らない。
XINUSE
xrstor は、使ったレジスタだけを復元するようになっており、xsaveopt は、使ったレジスタだけを保存するようになっている。(xsave は指定されたレジスタを全て保存する)
この使ったレジスタを追跡している状態が、XINUSE だ。XINUSE は、XCR0 と同じように、_xgetbv で取得できる。
#include <stdio.h>
#include <immintrin.h>
__m256i xyzzy;
int main(int argc, char **argv)
{
auto xcr0 = _xgetbv(0);
auto xinuse = _xgetbv(1);
printf("XCR0 : %016llx\n", (long long)xcr0);
printf("XINUSE : %016llx\n", (long long)xinuse);
puts("=== use mmx ===");
_mm_empty();
xinuse = _xgetbv(1);
printf("XINUSE : %016llx\n", (long long)xinuse);
puts("=== use avx ===");
xyzzy = _mm256_set1_epi8(argc);
xinuse = _xgetbv(1);
printf("XINUSE : %016llx\n", (long long)xinuse);
}
$ ./xinuse
XCR0 : 0000000000000207
XINUSE : 0000000000000202
=== use mmx ===
XINUSE : 0000000000000203
=== use avx ===
XINUSE : 0000000000000207
レジスタに触るとXINUSEが設定されている。
関連して、vzeroupper 命令は、XINUSEのAVXの使用中状態を消去できる。vzeroall はAVX,SSEの両方の使用中状態を消去できる。
#include <stdio.h>
#include <immintrin.h>
__m256i xyzzy;
int main(int argc, char **argv)
{
auto xcr0 = _xgetbv(0);
auto xinuse = _xgetbv(1);
printf("XCR0 : %016llx\n", (long long)xcr0);
printf("XINUSE : %016llx\n", (long long)xinuse);
puts("=== use mmx ===");
_mm_empty();
xinuse = _xgetbv(1);
printf("XINUSE : %016llx\n", (long long)xinuse);
puts("=== use avx ===");
xyzzy = _mm256_set1_epi8(argc);
xinuse = _xgetbv(1);
printf("XINUSE : %016llx\n", (long long)xinuse);
puts("== vzeroupper ==");
_mm256_zeroupper();
xinuse = _xgetbv(1);
printf("XINUSE : %016llx\n", (long long)xinuse);
puts("== vzeroall ==");
_mm256_zeroall();
xinuse = _xgetbv(1);
printf("XINUSE : %016llx\n", (long long)xinuse);
}
$ ./vzero
XCR0 : 0000000000000207
XINUSE : 0000000000000202
=== use mmx ===
XINUSE : 0000000000000203
=== use avx ===
XINUSE : 0000000000000207
== vzeroupper ==
XINUSE : 0000000000000203
== vzeroall ==
XINUSE : 0000000000000201
これは、実行ファイル全体をVEX付きでコンパイルしていて、SSE-AVXの切り替えペナルティが無い場合でも、vzeroupper を実行すればコンテキストスイッチのコストを下げられるということである。(xsaveoptは、XINUSEが立ってるところだけを保存し、xrstorは保存されたところだけを復元するので)
(つまりAVXレジスタを使い終わったら常にvzeroupperを実行しておくべき)
xsave/xrstor
xsave
命令は、使ったレジスタを保存する命令で。xrstor
は、保存されたレジスタを復元し、保存されていないレジスタを初期状態に戻す命令だ。
xsave
,xrstor
ともに、EDX:EAX として、64bit のstate-component bitmapを受け取り、それと現在のXCR0との and をとって、RFBM(requested-feature bitmap) とする。
xsave は、このRFBMで指定されたレジスタセットが、オペランドで指定されたアドレスに保存される。
xrstor は、オペランドで指定されたアドレスに保存された状態から、RFBMで指定されたレジスタセットが復元される。
- xsave
- EDX:EAX と XCR0 の and をRFBMとする
- RFBM で指定されたレジスタセットをオペランドで指定されたアドレスに保存する
- xrstor
- EDX:EAX と XCR0 の and をRFBMとする
- RFBM で指定されたレジスタセットをオペランドで指定されたアドレスから復元する
xsave
は、レジスタセットと同時に、保存したレジスタセットを示すビット列を XSAVE header に XSTATE_BV
として保存する。XSAVE header は、オペランドで指定したアドレス + 512byte のところに配置されている。
xrstor
は、復元時に、XSTATE_BV を見て、保存されているレジスタセットを復元する。XSTATE_BV で指定されないレジスタセットは、initial configurationに設定される。initial configuration の詳細はマニュアルを見たほうがよいが、基本的にデータレジスタはゼロ、FPUの制御レジスタは決められた初期設定になる。(これを使えばx87 FPUも初期状態に戻せる)
xsaveopt
(注意: Intelのマニュアルでは、xsaveopt はユーザ空間で使うのは推奨されていません。どうしても必要な場合はよく理解して使いましょう。xsaveoptと同じように保存を省略するxsavecも同様です)
保存するレジスタセットは、XINUSEとXMODIFIED見て決められる。これにより、xsaveopt
と xrstor
は、「使ったレジスタだけを保存、復元する」という機能を実現している。
昔は割り込みを使ってコンテキストスイッチを遅延させて、FPUのスイッチコストを削減していたが、これは必要なくなっている(ハードウェアが自動でやってくれる)。
Discussion