Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Xbyakの紹介とその周辺

779 views

Published on

カーネル/VM探検隊@関西 9回目

Published in: Software

Xbyakの紹介とその周辺

  1. 1. Xbyakの紹介とその周辺 カーネル/VM探検隊@関西 9回目 2018/9/22 光成滋生
  2. 2. • @herumi • https://github.com/herumi/she-wasm • ペアリングベースの準同型暗号のWebAssembly向け実装 • IEEE trans. on Computers 2014, AsiaCCS 2018など • https://github.com/herumi/bls • BLS署名の実装 • Dfinityというブロックチェーン系ベンチャーが使ってる 自己紹介 2 / 45
  3. 3. • C++用のx86/x64専用JITアセンブラ • https://github.com/herumi/xbyak • 自分が使いたい(暗号用に開発を始めた)アセンブラ • 開発を初めてもうすぐ12年目 • AVX-512フルサポート • ちなみに現在(2018/9)Intelの一番長い名前の命令は • vgf2p8affineinvqb(17文字) • Galois Field Affine Transformation Inverse • 𝐹28の元𝑥に対して𝐴𝑥−1 + 𝐵を計算する Xbyakって何? 3 / 45
  4. 4. • NASM, gasなどの通常の静的なアセンブラに比べて • コードを書きやすい(個人の感想) • C++との連係がしやすい(個人の...) • VMを書きやすい(略) • V8やWebkitなどのJavaScriptエンジンも同等の JITアセンブラを持ってる(ARMなどにも対応してる) 従来のアセンブラとの比較 4 / 45
  5. 5. • 整数nを返す関数を生成するクラス • インスタンスを作成し関数ポインタを取り出して実行 実行時に整数nを返す関数を生成 struct Code : Xbyak::CodeGenerator { Code(int n) { mov(eax, n); // 注意 インラインアセンブラではない ret(); // 純粋なC++のコード } }; Code c1(3); auto f = c1.getCode<int (*)()>();// intを返す関数ポインタ printf("%d¥n", f()); // 3 実行時にコード生成 mov eax, 3 ret 5 / 45
  6. 6. • MASMに似せるための各種演算子オーバーロード • Intel命令をそのまま書けるので直感的に操作しやすい • 小さなブロックを組み合わせて作る感覚が楽しい 雰囲気 void gen_add(const RegExp& dst, const RegExp& src); void f(int n) { auto addr = n > 0 ? rsi + rax * 8 + n * 8 : rdi; mov(rax, ptr[addr]); vgatherqpd(zmm5 | k7, ptr [rax + 64 + zmm21 * 2]); gen_add(rsp + 8, rsi + 8); } 6 / 45
  7. 7. • template引数で長さを指定する整数クラス • 最大の大きさを指定するNはコンパイル時指定 • 実際の整数の大きさを指定するnは実行時指定 • このようなNを外部アセンブラと連係するのは面倒 固定多倍長加算 template<size_t N> struct Int { void init(size_t n); // n * 64 bit整数として利用(n < N) uint64_t d[N]; // z = x + y void (*add)(Int& z, const Int& x, const Int& y); }; 7 / 45
  8. 8. • 64 * n-bit加算(ビット長に応じたコード生成) 多倍長加算の例 GenAdd(int n) { for (int i = 0; i < n; i++) { mov(rax, ptr [x+i*8]); if (i == 0) add(rax, ptr [y+i*8]); else adc(rax, ptr [y+i*8]); mov(ptr [z+i*8], rax); } ret(); } add3: mov rax, [rsi] add rax, [rdx] mov [rdi], rax mov rax, [rsi + 8] adc rax, [rdx + 8] mov [rdi + 8], rax mov rax, [rsi + 16] adc rax, [rdx + 16] mov [rdi + 16], rax ret add2: mov rax, [rsi] add rax, [rdx] mov [rdi], rax mov rax, [rsi + 8] adc rax, [rdx + 8] mov[rdi + 8], eax ret N=2 N=3 8 / 45
  9. 9. • LLVMのbitコード(VMのコード)で書けば各種 CPUへの最適化コードが出力されるバラ色の世界 • と思っていたときもあった • 意外と各種アーキテクチャに縛られる • x64向けにはコード生成されるがARMではランタイムエラー • WebAssemblyへもランタイムエラー • 32bit/64bit CPU向けに別々にコードを書く必要(私の経験) • VMとは • 手書き最適化に比べて1~2割遅い(ことが多い) • Intel専用命令使いたい・勝手に使われたくない • もちろん開発コスト比を考慮すると○ LLVMのJITでええんじゃね? 9 / 45
  10. 10. • LLVMの例 • x64での128bit演算出力 • 命令順序を除いて先ほどのコードと同一コードを生成 • 1024bitなら? 128~1024bitの加算 define void @add128(i128* %pz, i128* %px, i128* %py) { %x = load i128, i128* %px %y = load i128, i128* %py %z = add i128 %x, %y store i128 %z, i128* %pz ret void } 10 / 45
  11. 11. • 怒濤のレジスタスピル(溢れ) • xとyを足してzに配置するとき 下位レジスタから順にやれば データの退避は不要 • それを把握していないので 一度スタックにコピーしてる • 無駄にSIMDを使って遅くなる こともある • 余談 • llcに-pre-RA-sched=list-ilp -max-sched-reorder=16 でspillしなくなる(たまたま?) • llc --help-hiddenすると大量の隠しオプションが現れる • バージョン非互換なものも多い 1024bitの加算の出力 movq 16(%rsi), %r13 movq (%rsi), %rbx movq 8(%rsi), %r15 movq 120(%rdx), %rax movq %rax, -8(%rsp) # 8-byte Spill movq 112(%rdx), %rax movq 104(%rdx), %rcx movq %rcx, -24(%rsp) # 8-byte Spill movq 96(%rdx), %rcx movq 88(%rdx), %rbp movq %rbp, -32(%rsp) # 8-byte Spill movq 80(%rdx), %r8 movq 72(%rdx), %r12 movq 64(%rdx), %r14 movq 56(%rdx), %rbp addq (%rdx), %rbx movq %rbx, -16(%rsp) # 8-byte Spill ... 11 / 45
  12. 12. • PS2エミュレータ PCSX2 • https://github.com/PCSX2/pcsx2 ; plugin/shaderまわり • (当時)画像処理部分を画面のRGBのデータ並び(ビデオカ ードごとに違ってたりした)に応じた最適なコードをtempate を使ってコンパイル時コード生成 • バイナリコードの肥大化 • 実行時生成にすることでバイナリコードの削減と高速化 • mrubyのJIT by @miura1729さん • https://github.com/miura1729/mruby/ • 解説記事 https://qiita.com/miura1729/items/a1828849ec8fec596e74 • マンデルブロートなどの計算主体なものはx10ぐらいらしい 利用例 12 / 45
  13. 13. • 正規表現JITエンジン by @sinya8282 • https://github.com/sinya8282/Regen • JavaScript VMのJIT by @Constellation • https://github.com/Constellation/iv/ • http://labs.cybozu.co.jp/youth.html サイボウズ・ラボユース生によるJIT 13 / 45
  14. 14. • Citra(shaderのJIT部分に使ってるもよう) • https://github.com/citra-emu/citra • https://github.com/citra- emu/citra/blob/master/src/video_core/shader/shader_jit_x64_ compiler.cpp • log2やexp2なども実行時コード生成してる 3DSエミュレータ SSE4.1が使えれば それを利用 無ければ代替命令を 生成 14 / 45
  15. 15. • 機械学習・深層学習ライブラリ • https://github.com/intel/mkl-dnn • 最新CPUのAVX-512命令v4fmaddpsなども利用 • jit_avx512_common_conv_winograd_kernel_f32.cpp • templateとlambdaとアセンブラの混在感が味わい深い? Intel MKL-DNN 15 / 45
  16. 16. • 自動パフォーマンスチューニングワークショップの Intelの招待講演 • http://iwapt.org/2018/iwapt2018_proceedings/SarahKnepper_j it_compilation_iwapt.pdf • ありがたいことにほとんどXbyakの使い方 iWAPT2018 16 / 45
  17. 17. • 直感的にJITコードを記述できる • 前述のPDFから 行列演算の端数処理 生成コード 17 / 45
  18. 18. • CPUのL2, L3キャッシュサイズを取得 • https://github.com/intel/mkl- dnn/blob/19588d1484911a3dc7933b32ce71d2f1b9bbbb78/sr c/cpu/jit_avx512_core_fp32_wino_conv_2x3.cpp#L580 • データサイズやレジスタの個数を計算しながら適切な ループサイズを計算してJITしてる模様(詳細は未読) キャッシュサイズを意識 const int L2_cap = get_cache_size(2, true) / sizeof(float); const int L3_capacity = get_cache_size(3, false) / sizeof(float); 18 / 45
  19. 19. • Intelによる小さい行列計算専用JITライブラリ • https://github.com/hfp/libxsmm • Cでゼロから作られてる(without Xbyak) • https://www.ixpug.org/images/docs/IXPUG_Annual_Spring_Co nference_2018/09-PABST-Libxsmm.pdf LIBXSMM 19 / 45
  20. 20. • メモリを確保 • malloc/_aligned_malloc/posix_memalign • 命令フォーマットにしたがってバイトコードを展開 • modRM, SIB, rex, vex, evex, etc., AVX-512は結構大変 • exec属性を付与して実行 • mprotect(Linux), VirtualProtect(Win) • ぶっちゃけ原理は簡単(作り込みは大変) • だが10年以上やってるといろいろ経験する Xbyakの実装 20 / 45
  21. 21. • jnl(jump if not less)がIntelコンパイラでエラー • UNIX系コンパイラは各種特殊数学関数を持っている • jnlは次数nの第1種Bessel関数のlong double版 • j1とかy0とかynなど、そんなグローバル関数が!と思うもの がいろいろ • https://www.gnu.org/software/libc/manual/html_node/Speci al-Functions.html • コンパイラの実装によっては関数はマクロでもよいらしい • C++17ではまともな名前でcmathに登場 • j1 → std::cyl_bessel_j(1, x) • yn(n, x) → std::neumann(n, x) いろいろなトラブル 21 / 45
  22. 22. • and関数を作る • もちろんCでコンパイルできる • がC++ではエラー gccで通るがg++で通らないコード >g++ -c c++ t.c t.cpp:1:9: error: expected unqualified-id before 'int' int and(int x, int y) ^~~ t.cpp:1:9: error: expected ')' before 'int' t.cpp:1:9: error: expected initializer before 'int' >gcc -c t.c int and(int x, int y) { return x & y; } 22 / 45
  23. 23. • C++ではand, or, xor, not, and_eq, bitorなどが予約語 • 関数名に使えない • VCではiso646.hをincludeしないなら使える • gcc/clangでは-fno-operator-namesオプションで無効化 • うっかり忘れると一見意味不明な大量のエラー • Xbyakでは • and_(), or_()などアンダースコアをつけた名前に変更 • 後方互換性のためand(), or()などもサポート • -fno-operator-namesなしで使おうとすると "use -fno-operator-names option"という#errorを表示してる • どうやって? 代替表現(Alternative representations) 23 / 45
  24. 24. • プリプロセッサの仕様を利用する • notが予約語(operator~の代替表現)の場合 • #ifの後ろは~+0 = -1 ≠ 0となりtrueなので#errorで止まる • notが予約語でない場合 • プリプロセッサの中で定義されないマクロの値は0 • 0 +0 = 0で#ifが実行されない トリック(by @digitalghost) #if not +0 #error "use -fno-operator-names" #endif 24 / 45
  25. 25. • N = 32700ぐらいでエラー • WindowsでならNがもっと大きくても動く • メモリが足りないわけではない Linuxでたくさんnewできないという報告 struct Code : Xbyak::CodeGenerator { Code(int x) { mov(eax, x); ret(); } }; std::vector<std::unique_ptr<Code>> v(N); for (int i = 0; i < N; i++) { v[i] = std::make_unique<Code>(i); } 25 / 45
  26. 26. • 1プロセスあたりのメモリマップの上限 • デフォルト65536 • Xbyakのposix_memalign + mprotectは2個消費する • スレッドも1スレッドあたり2個消費する • 上限に達するとmprotectはENOMEMを返す • XBYAK_USE_MMAP_ALLOCATORを定義すると大丈夫 • posix_memalignの代わりにmmapを使うallocator • 何故かこの上限に掛からなくなる • https://www.kernel.org/doc/Documentation/sysctl/vm.t xtにはmmap, mprotect, madviseの呼び出しに影響とある • 70万個とかでも作れるようになる /proc/sys/vm/max_map_count 26 / 45
  27. 27. • 最初は従来のアセンブラのラベルを模倣 • ローカルラベル • MASMライクな@@, @b, @f 古典的なラベル L("loop"); ... dec(ecx); jnz("loop"); inLocalLabel(); // ピリオドで始まるラベルは L(".lp"); jmp(".lp"); outLocalLabel(); // この区間内でだけ有効なローカルラベル 27 / 45
  28. 28. • JITならではの要件 • ラベルは文字列ではなく変数であってほしい 柔軟なラベル Label generate_function(int type) { Label entry = L(); // typeに応じて関数生成 return entry; } Label add = generate_function(TypeAdd); Label sub = generate_function(TypeSub); Label mul = generate_function(TypeMul); call(add); 28 / 45
  29. 29. • ラベルを即値として扱う ジャンプテーブルも作りたい Label labelTbl, L0, L1, L2; mov(rax, labelTbl); // アドレスを代入 jmp(ptr [rax + rcx * sizeof(void*)]); jmp(ptr [rip + L0]);// L0への相対アドレッシングジャンプ // ジャンプテーブル L(labelTbl); putL(L0); // ラベルのアドレスをメモリに配置 putL(L1); L(L0); mov(a, ret0); ret(); L(L1); ... 29 / 45
  30. 30. • コード生成時にジャンプ先は未定 • その後エラー処理コードを生成してから generate_function()のエラー先を決定したい プレースフォルダ的な使い方 std::pair<Label, Label> generate_function() { Label entry = L(); Label error; .. jmp(error); // エラー処理に飛ぶ(が飛び先未定) return {entry, error}; } 30 / 45
  31. 31. • 2個のラベルをリンクさせる assignL(dstLabel, srcLabel); {add, err1} = gen_func(...); // 飛び先未定のラベル {sub, err2} = gen_func(...); // 飛び先未定のラベル closeErr = L(); // closeのエラー処理 criticalErr = L(); // criticalエラーの処理 asignL(err1, closeErr); //add関数内のエラーはcloseErr asignL(err2, criticalErr); //sub関数のエラーはcriticalErr 31 / 45
  32. 32. • 8文字からなる言語 • [ ; ポインタが示す値が0なら]にジャンプ • ] ; 対応する[にジャンプ • while (*cur) { ... }に相当 BrainfuckのJIT stack<Label> labelB, labelF; case '[': labelB.push(L()); mov(eax, cur); test(eax, eax); Label F; jz(F, T_NEAR); labelF.push(F); break; case ']': jmp(labelB.top()); labelB.pop(); L(labelF.top()); labelF.pop(); break; B: // [ mov rax, [rcx] test eax, eax jz F ... // ネストする jmp B // ] F: 32 / 45
  33. 33. • ラベルがL()でアドレス確定されるごとに • そのラベルを参照している全ての未定義一覧のアドレス解決 • ラベルがジャンプ命令で指定されるごとに • 既にラベル先が確定しているものはアドレス確定 • その時点で行き先が未定義のもののものは未定義一覧に追加 • + 相対ジャンプで管理すべきものもある • assignL()はリンクを変更 • ローカルラベルやスコープを抜けたラベルは管理外に • 数が多いので保持し続けるとラベル解決の速度劣化に • ラベルはコピーされるとスコープの外に出ることがある • 参照カウンタで管理 ラベル管理の内部 33 / 45
  34. 34. • L()やjmp(label)が呼ばれるごとに • ローカルラベルやスコープを抜けたラベルは管理外に • 数が多いので保持し続けるとラベル解決の速度劣化の要因 • ラベルはコピーされるとスコープの外に出ることがある • 参照カウンタで管理 ラベル管理の内部 未定義ラベルULの集合U ULにリンクするjmpの場所を保持 定義済みラベルDLの集合D ULにリンクするjmpのaddrを解決 L(UL)で ラベル追加 jmp(label)のlabelが Dに無い Dにある→addr解決 jmp(UL); ... jnc(UL); ... putL(UL); assignL(UL, DL);で ラベル移動 34 / 45
  35. 35. • JITコードはプロファイラを使っても分からない • VTuneなどはそれぞれJITコードを教えるAPIがある • cf. http://herumi.in.coocan.jp/prog/profile.html#USEVT プロファイラ 35 / 45
  36. 36. • perfの場合/tmp/perf-<pid>.mapに1行ずつ を書いておくと集計時に自動的に利用してくれる perf with JIT code アドレス サイズ 名前 PerfMap::set(const void *p, size_t n, const char *name) { fprintf(fp, "%llx %zx %s¥n", (long long)p, n, name); } PerfMap pm; pm.set(c.getCode(), c.getSize(), "fff"); pm.set(c2.getCode(), c2.getSize(), "ggg"); 36 / 45
  37. 37. • testはnasm, yasmなどのツールの出力と比較して確認 • 新命令はツールが間違ってることが多いので悩ましい • nasmは何度もバグ報告してる • 昔はyasmの方が信頼性が高かったが最近更新されてない • Intelのマニュアルが間違ってることもある • 印象に残っているバグをいくつか • VM上で未定義命令エラー • cpuidを見てCPUが新命令に対応している判別してコード生成 • host CPUはその命令に対応しているがgestのVMは非対応 しかしVMはhostのcpuidを返していた • 生成された命令を実行してillegal instruction(ややこしい) バグ 37 / 45
  38. 38. • t.asm • yasm -f win32 -l t.lst t.asm • EBFEではなくEB00が正解 • 生成オブジェクトは正しいEB00を出力 • 正しく実行はできるので結構悩んだ • チケットには2011年に登録されていた • https://tortall.lighthouseapp.com/projects/78676/tickets/233 -byte-code-in-listing-differs-from-emitted-bytes • 実は最新版でも直ってない yasmのlstとobjの不一致 jmp L L: 1 %line 1+1 t.asm 2 00000000 EBFE jmp L 3 L: 38 / 45
  39. 39. • 別の命令(1to2)を挟むと逆アセンブル結果がバグる • 最初は正しい出力なので混乱した • https://sourceware.org/bugzilla/show_bug.cgi?id=23025 • 報告して10時間でpatchが作成された objdumpの逆アセンブル出力 >objdump -M x86-64 -D -b binary -m i386 vcvtpd2dq.bin 67 c5 fb e6 40 20 vcvtpd2dqx 0x20(%eax),%xmm0; (X) 67 c5 ff e6 40 20 vcvtpd2dqy 0x20(%eax),%xmm0; (Y) 67 62 f1 ff 18 e6 40 04 vcvtpd2dq 0x20(%eax){1to2},%xmm0 67 c5 fb e6 40 20 vcvtpd2dq 0x20(%eax),%xmm0 ; (X') 67 c5 ff e6 40 20 vcvtpd2dq 0x20(%eax),%xmm0 ; (Y') 39 / 45
  40. 40. • vgatherdps(zmm0|k1, ptr [rax + zmm18]); が vgatherdps(zmm0|k1, ptr [rax + zmm2]);になるバグ • VSIBエンコーディング • 従来のSIB ; [eax + ebx * scale + offset] ; レジスタ8種類 • 64bit対応 ; 16種類レジスタを表現するため1bit増える • その1bitはREXプレフィックスの中に • VSIB ; AVX2でSIMDレジスタを指定できるようになった XbyakのVSIBエンコーディングバグ SDM2.3.12 Vector SIB(VSIB) Memory Addressing 40 / 45
  41. 41. • VSIBやREXでは足りない • ?mm16~?mm31までのレジスタはどうやって指定? • EVEX.vvvvビット • addpd(zmm31, zmm30, zmm20);などはちゃんと実装していた • SDM2.6.1 Instruction Format and EVEX • VSIBのときEVEX.V'をoffにするのだった • 単なる見落とだがVSIB Memory Addressingのところの記述は 変わってないし……(言い訳) AVX-512でレジスタは32個 EVEXV’ High-16 NDS/VIDX register specifier P[19] Combine with EVEX.vvvv or when VSIB present. 41 / 45
  42. 42. • cpuid(eax=0x0b, ecx=0/1)でSMTやCOREの数を取得 • cpuid(eax=0x4, ecx=cache_level)で各階層の情報を取得 • VMのせいか時々おかしい値に キャッシュサイズの取得はややこしい 42 / 45
  43. 43. • Intel Software Development Emulator • https://software.intel.com/en-us/articles/intel-software- development-emulator • 当たり前だが一番信頼できる • xed -mpx -64 -ir <rawobj>でdisassemblerとして利用可能 • vgatherdps(zmm0|k1, ptr[rax + zmm2]); の出力は正しくdisasできるのに vgatherdps(zmm0|k1, ptr[rax + zmm0]); の出力はエラー • 両方とも同じエンコードパスを通るのに何故? Intel SDEの仕様に悩む ERROR: GATHER_REGS Could not decode at offset: 0x0 PC: 0x0: [62F27D49920400] 43 / 45
  44. 44. • gather命令はindex == destinationのとき使えない • Note that: If any pair of the index, mask, or destination registers are the same, this instruction results a UD fault. • decode errではなく実行時エラーならよかったんだが エンコードは正しいがUDのため弾かれた 44 / 45
  45. 45. • 自分で小さいVM作ってみると面白いかも • 狙ったコードを生成できると嬉しい • 命令数が多い(マニュアルが分厚い)と大変だ まとめ 45 / 45

×