🌟

xsave

2021/12/14に公開

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 headerXSTATE_BV として保存する。XSAVE header は、オペランドで指定したアドレス + 512byte のところに配置されている。

xrstor は、復元時に、XSTATE_BV を見て、保存されているレジスタセットを復元する。XSTATE_BV で指定されないレジスタセットは、initial configurationに設定される。initial configuration の詳細はマニュアルを見たほうがよいが、基本的にデータレジスタはゼロ、FPUの制御レジスタは決められた初期設定になる。(これを使えばx87 FPUも初期状態に戻せる)

xsaveopt

(注意: Intelのマニュアルでは、xsaveopt はユーザ空間で使うのは推奨されていません。どうしても必要な場合はよく理解して使いましょう。xsaveoptと同じように保存を省略するxsavecも同様です)

保存するレジスタセットは、XINUSEとXMODIFIED見て決められる。これにより、xsaveoptxrstorは、「使ったレジスタだけを保存、復元する」という機能を実現している。
昔は割り込みを使ってコンテキストスイッチを遅延させて、FPUのスイッチコストを削減していたが、これは必要なくなっている(ハードウェアが自動でやってくれる)。

xsavec

GitHubで編集を提案

Discussion