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.
フラグを愛でる2013/3/30 光成滋生(@herumi)x86/x64最適化勉強会5(#x86opti)
目次 さまざまなコード片でcarryの扱いを味わってみる   フラグの復習   絶対値の復習   ビットの長さ   ビットカウント     cybozu/WaveletMatrixの紹介   多倍長整数加算   Haswellで...
フラグの復習 演算ごとに変化する1bitの情報群 よく使うもの   ZF : Zero flag : 演算結果が0ならtrue   CF : Carry flag : 演算に桁上がり、桁借りがあればtrue   SF : Sign f...
絶対値(1/3) int abs(int x) { return x >= 0 ? x : -x; }   -x = ~x + 1   ~x = x ^ (-1)   組み合わせると –x = x ^ (-1) – (-1)   また...
絶対値(2/3) gcc 4.7での実装       // input : ecx, output : eax       // destroy : edx, eax       mov   edx,      ecx   ;   edx  ...
絶対値(3/3) VCでの実装       mov eax, ecx       cdq             ; M = edx = (eax < 0) ? -1 : 0       xor eax, edx    ; eax = x^ ...
ビットの長さ(1/4) xを確保するのに必要なビットの長さ   x == 0のとき1とする       int bitLen(uint32_t x) {           if (x == 0) return 1;           f...
ビットの長さ(2/4) gcc 4.7とclang 3.3        // gcc                   // clang           test edi, edi         mov eax, 1        ...
ビットの長さ(3/4) なぜかgccだけよくなった   xorとaddがキャンセルした         //       gcc                   // clang                  test edi, e...
ビットの長さ(4/4) VCはbsrは入力が0ならZF=1なことを知っている                bsr    eax, ecx                je     .zero                inc    e...
減算のあとのmod p(1/2) 暗号ではX={0, 1, ..., p-1}の中の四則演算をよく使う   x, y ∈ Xに対して(x + y) % pとか(x – y) % pとか        // 引き算の疑似コード        ...
減算のあとのmod p(2/2) sub + sbb後のCFを利用してaddすべき値をcmov       // 疑似コード       sub x0, y0       sbb x1, y1       sbb x2, y2       s...
128bit popcnt(1/3) Wavelet行列の中で使う簡潔ベクトル構造の中   結局, 今のところ使わなかったけど面白かったので紹介     idxから128bit分のマスクを作って[b1:b0]と&をとってpopcnt   ...
128bit popcnt(2/3) gcc 4.7   ジャンプ命令を使う…論外 VC2012   idx & 64 == 0の判定を2回する…おしい clang 3.3   idx & 64 == 0の判定は1回だがなぜかシフト...
128bit popcnt(3/3) ちょっと宣伝   SucVectorとWaveletMatrixクラス開発中     実装済みメソッド : get, rank, rankLt, select     Yasuo.Tabeiさんのf...
多倍長整数の加算(1/5) 前半発表のlliの出力(一部)         .lp: mov r9,   qword [rsi]       ;   x[i]              add       r9, qword [rdx]   ...
多倍長整数の加算(2/5) 加算でやらしいところ   carryつきaddを実行したいのでcarryを変更してはいけない   でもループ変数はいじらないといけない   コンパイラに任せると先程のフラグを保存するコードになる 抜け道  ...
多倍長整数の加算(3/5) Xeon X5650(Westmere)では   ループあたり13clkもかかる     なんと先程のLLVMの(酷い)コードよりも遅い!   フラグに関するパーシャルレジスタストール     Intelの...
多倍長整数の加算(4/5) jrcxz/jecxz命令   rcx, ecxがゼロなら分岐する命令     みなさん覚えてますか? 私は忘れてました         Pentiumで遅かったのでloop命令とともに封印された(私の中で)...
多倍長整数の加算(5/5) Sandy Bridgeでは改良された   元のコード(adc + dec)の方が速い   先頭だけ外に出す微調整でもっと速くなった(なぜ)  1024bit(64x16)加算          adc + d...
Haswellで追加された命令(1/3) 無視されるフラグたち                       命令 動作                       adcx 符号なし加算(CFのみ変更)                    ...
Haswellで追加された命令(2/3) ビット操作系   andn(x, y) = ~x & y     今更感が   bextr(x, start, len)     xの[start+len-1:start]のビットを取り出す(...
Haswellで追加された命令(3/3)   pdep(x, mask)     maskのビットがたっているところにxのビットを埋め込む                   x   x5   x4   x3   x2   x1   x0 ...
おまけ 今どきのコンパイラはかしこい ハッシュ関数(fnv-1a)のループ内演算   https://github.com/herumi/misc/blob/master/fnv-1a.cpp       for (size_t i = ...
Upcoming SlideShare
Loading in …5
×

フラグを愛でる

5,808 views

Published on

Published in: Technology
  • Be the first to comment

フラグを愛でる

  1. 1. フラグを愛でる2013/3/30 光成滋生(@herumi)x86/x64最適化勉強会5(#x86opti)
  2. 2. 目次 さまざまなコード片でcarryの扱いを味わってみる  フラグの復習  絶対値の復習  ビットの長さ  ビットカウント cybozu/WaveletMatrixの紹介  多倍長整数加算  Haswellで追加された命令達 題材があちこち飛びます m(__)m2013/3/30 #x86opti 5 2 /24
  3. 3. フラグの復習 演算ごとに変化する1bitの情報群 よく使うもの  ZF : Zero flag : 演算結果が0ならtrue  CF : Carry flag : 演算に桁上がり、桁借りがあればtrue  SF : Sign flag : 演算結果が負ならtrue例 mov eax, 5 neg eax ; -5になるのでSF = 1, ZF = 0 mov eax, 0x80000001 mov ecx, 0x80000002 add eax, ecx ; 32bitを超えるのでCF = 1 条件つきmov命令  フラグの条件が成立すれば代入 cmovz eax, ecx ; ZF = 1ならeax ← ecx2013/3/30 #x86opti 5 3 /24
  4. 4. 絶対値(1/3) int abs(int x) { return x >= 0 ? x : -x; }  -x = ~x + 1  ~x = x ^ (-1)  組み合わせると –x = x ^ (-1) – (-1)  またx ^ 0 – 0 = x  xが0以上の時も、xが負のときもx ^ M – Mの形をしている  Mを作れればabsを分岐なしで実行できる 古典的なトリック int abs(int x) { uint32_t M = (x >> 31); // M = x >= 0 ? 0 : (-1); return x ^ M – M; }2013/3/30 #x86opti 5 4 /24
  5. 5. 絶対値(2/3) gcc 4.7での実装 // input : ecx, output : eax // destroy : edx, eax mov edx, ecx ; edx = x sar edx, 31 ; edx = (x < 0) ? -1 : 0; mov eax, edx ; eax = M xor eax, ecx ; eax ^= x ; eax = x ^ M sub eax, edx ; eax -= M ; eax = x ^ M - M clang 3.3での実装 mov eax, ecx ; eax = x mov edx, ecx ; edx = x neg eax ; eax = -x cmovl eax, edx ; eax = (x > 0) ? x : -x  わかりやすい  sandyだと少し速い(古いCPUだとgccのがよいことも)2013/3/30 #x86opti 5 5 /24
  6. 6. 絶対値(3/3) VCでの実装 mov eax, ecx cdq ; M = edx = (eax < 0) ? -1 : 0 xor eax, edx ; eax = x^ M sub eax, edx ; eax = x^ M - M  cmovより速い(ことが多い) cmov命令はIntel CPUではものすごく速いというわけではない core2duo, sandy bridgeと段々よくなったが  レジスタ固定だが  まあいまどきmovはほぼコスト0だし  64bitだとcqo(0x48 0x99) VCのcod出力はcdqと表示されるので注意2013/3/30 #x86opti 5 6 /24
  7. 7. ビットの長さ(1/4) xを確保するのに必要なビットの長さ  x == 0のとき1とする int bitLen(uint32_t x) { if (x == 0) return 1; for (int i = 0; i < 32; i++) { if (x < (1u << i)) return i; } return 32; } __builtin_clzを使う  これはcount leading zeroなので32から結果を引く  この関数はx == 0のときは未定義なので別に処理する if (x == 0) return 1; return 32 - __builtin_clz(x);2013/3/30 #x86opti 5 7 /24
  8. 8. ビットの長さ(2/4) gcc 4.7とclang 3.3 // gcc // clang test edi, edi mov eax, 1 mov eax, 1 test edi, edi je .Z je .Z bsr edi, edi bsr eax, edi mov al, 32 xor eax, -32 xor edi, 31 add eax, 33 sub eax, edi .Z: ret .Z: ret clangの方がちょっと賢い感じだがなんか微妙  bsr(x) == 32 - __builin_clz(x)なので回りくどい 少し変えてみる if (x == 0) return 1; // return 32 - __builtin_clz(x); return (__builtin_clz(x) ^ 0x1f) + 1;2013/3/30 #x86opti 5 8 /24
  9. 9. ビットの長さ(3/4) なぜかgccだけよくなった  xorとaddがキャンセルした // gcc // clang test edi, edi mov eax, 1 mov eax, 1 test edi, edi je .Zero je .Zero bsr edi, edi bsr eax, edi add eax, 1 xor eax, -32 add eax, 33 .Zero: ret .Zero: ret VCでは_BitScanReverse(&ret, x)を使う  これはx == 0のときfalseを返す unsigned long ret; if (_BitScanReverse(&ret, x)) return ret + 1; return 1;2013/3/30 #x86opti 5 9 /24
  10. 10. ビットの長さ(4/4) VCはbsrは入力が0ならZF=1なことを知っている bsr eax, ecx je .zero inc eax ret .zero: mov eax, 1 ret いや, でもZF = 1のときはecx = 0なんだし こうすればすっきりする bsr eax, ecx cmovz eax, ecx inc eax ret  ただしx == 0が殆どありえないなら上の方が速いかも2013/3/30 #x86opti 5 10 /24
  11. 11. 減算のあとのmod p(1/2) 暗号ではX={0, 1, ..., p-1}の中の四則演算をよく使う  x, y ∈ Xに対して(x + y) % pとか(x – y) % pとか // 引き算の疑似コード // const uint255_t p = ...; uint255_t sub(uint255_t x, uint255_t y) { if (x >= y) return x – y; return x + p – y; }  大小比較って結局は引いてみないと分からない uint255_t add(uint255_t x, uint255_t y) { int256_t t = x – y; if (t < 0) t += p; return t; }  分岐はランダムなので10clk以内なら条件jmpは避けたい2013/3/30 #x86opti 5 11 /24
  12. 12. 減算のあとのmod p(2/2) sub + sbb後のCFを利用してaddすべき値をcmov // 疑似コード sub x0, y0 sbb x1, y1 sbb x2, y2 sbb x3, y3 // [x3:x2:x1:x0] – [y3:y2:y1:y0] t0 = t1 = t2 = t3 = 0; cmovc [t3:t2:t1:t0], [p3:p2:p1:p0] ; t = (x < y) ? p : 0 [x3:x2:x1:x] += [t3:t2:t1:t0]  256bit減算なので64bitレジスタ x 4を使う 実際にはcmovなどが4個並んでる ルール : 分岐予測不可→cmov→可能なら単純命令に  0に設定してcmovよりマスクして&が少し速い(CPUによる) sbb m, m // m = (x < y) ? -1 : 0 [t3:t2:t1:t0] = [p3:p2:p1:p0] [t3:t2:t1:t0] &= m2013/3/30 #x86opti 5 12 /24
  13. 13. 128bit popcnt(1/3) Wavelet行列の中で使う簡潔ベクトル構造の中  結局, 今のところ使わなかったけど面白かったので紹介 idxから128bit分のマスクを作って[b1:b0]と&をとってpopcnt idx&127>=64なら[m1:m0]=[*:-1]。<64なら[m1:m0]=[0:*] | b0 | b1 | |0|1|2|3|4|5|6|7|8|9|a|b|c|d|e|f| m|***************|**** | idx & 127 >= 64 |********** | | idx & 127 < 64 | m0 | m1 | uint64_t maskAndPopcnt(uint64_t b0, uint64_t b1, uint64_t idx){ const uint64_t mask = (uint64_t(1) << (idx & 63)) - 1; uint64_t m0 = (idx & 64) ? -1 : mask; uint64_t m1 = (idx & 64) ? mask : 0; uint64_t ret = popcnt(b0 & m0); ret += popcnt(b1 & m1); return ret; }2013/3/30 #x86opti 5 13 /24
  14. 14. 128bit popcnt(2/3) gcc 4.7  ジャンプ命令を使う…論外 VC2012  idx & 64 == 0の判定を2回する…おしい clang 3.3  idx & 64 == 0の判定は1回だがなぜかシフト しかも作ったフラグをつぶす(clangあるある) // edx = idx ZFを保存するため 最適解? and edx, 64 順序入れ換える ecxとedxを入れ換えたら shr edx, 6 xor不要 xor ecx, ecx xor ecx, ecx or rcx, -1 ;3byte減る test edx, edx and edx, 6 and edx, 6 cmovneq rcx, rax cmovneq rcx,rax cmovneq rdx, rax mov rdx, -1 mov rdx, -1 cmoveq rdx, rax cmoveq rdx, rax cmoveq rcx, rax2013/3/30 #x86opti 5 14 /24
  15. 15. 128bit popcnt(3/3) ちょっと宣伝  SucVectorとWaveletMatrixクラス開発中 実装済みメソッド : get, rank, rankLt, select Yasuo.Tabeiさんのfmindex++に組み込んでみた 実験コード https://github.com/herumi/fmindex  200MBのUTF-8テキストから1400個の単語(平均12byte) の全出現位置列挙(locateの呼び出し24M回) 実装 時間[sec] lookup[clk] rank[clk] オリジナル(wat_array) 160 1887 1887 wavelet-matrix-cpp(wm) 72 883 598 cybozu/WaveletMatrix(cy) 30 343 183 wat_arrayは岡野原さん, wavelet-matrix-cppはmanabeさん作 wmに比べてもcyはrankが約3.2倍, lookupが2.5倍2013/3/30 #x86opti 5 15 /24
  16. 16. 多倍長整数の加算(1/5) 前半発表のlliの出力(一部) .lp: mov r9, qword [rsi] ; x[i] add r9, qword [rdx] ; +y[i] setb al ; al = carry ? 1 : 0 movzx r8d, r8b ; 一つ目のcarry and r8, 1 add r8, r9 ; x[i] + y[i] + carry mov qword [rdi], r8 ; 保存 add rsi, 8 add rdx, 8 add rdi, 8 dec rcx ; ループカウンタ mov r8b, al ; 今回のフラグを保存 jne .lp なんかえらいことに  実はとても遅いというわけではなかったり(たまたま)2013/3/30 #x86opti 5 16 /24
  17. 17. 多倍長整数の加算(2/5) 加算でやらしいところ  carryつきaddを実行したいのでcarryを変更してはいけない  でもループ変数はいじらないといけない  コンパイラに任せると先程のフラグを保存するコードになる 抜け道  add, sub, adc, sbbはCFとZFを変更するが inc, decはCFを変更しない  ループカウンタcを-nから0方向にインクリメント x, y, zのアドレスはあらかじめn * 8を足してずらしておく .lp: mov(t, ptr [x + c * 8]); // t = x[i] adc(t, ptr [y + c * 8]); // t = x[i] + y[i] + carry mov(ptr [z + c * 8], t); // z[i] = t inc(c); jnz(".lp");2013/3/30 #x86opti 5 17 /24
  18. 18. 多倍長整数の加算(3/5) Xeon X5650(Westmere)では  ループあたり13clkもかかる なんと先程のLLVMの(酷い)コードよりも遅い!  フラグに関するパーシャルレジスタストール Intelの最適化マニュアル 「INCとDECはADDやSUBに置き換えるべきだ」 置き換えたら動かないんですけど .lp: mov(t, ptr [x + rcx * 8]); // t = x[i] adc(t, ptr [y + rcx * 8]); // t = x[i] + y[i] + carry mov(ptr [z + rcx * 8], t); // z[i] = t sub(c, 1); // adcのキャリーを破壊する jnz(".lp");2013/3/30 #x86opti 5 18 /24
  19. 19. 多倍長整数の加算(4/5) jrcxz/jecxz命令  rcx, ecxがゼロなら分岐する命令 みなさん覚えてますか? 私は忘れてました  Pentiumで遅かったのでloop命令とともに封印された(私の中で) jnrcxzは無いのでループで使うとねじれるのが難  core2duo以降はそこそこ速い .lp: jrcxz(".exit"); mov(t, ptr [x + c * 8]); adc(t, ptr [y + c * 8]); mov(ptr [out + c * 8], t); lea(c, ptr [c + 1]); jmp(".lp");  16回ループ(1024bit加算)が208clk→62clk2013/3/30 #x86opti 5 19 /24
  20. 20. 多倍長整数の加算(5/5) Sandy Bridgeでは改良された  元のコード(adc + dec)の方が速い  先頭だけ外に出す微調整でもっと速くなった(なぜ) 1024bit(64x16)加算 adc + dec LLVM adc + jrcxz adc + dec(その2) Core2Duo(Win) 215 --- 55 221 Xeon X5650(Westmere) 208 63 62 202 sandy bridge 48 64 52 33  https://github.com/herumi/opti/blob/master/uint_add.cpp 多倍長の乗算 read+modify+writeが2clk/64bit  同様にmulがフラグを変更するのが邪魔  レジスタの使い回しで非常に苦労する  速度低下にもつながる2013/3/30 #x86opti 5 20 /24
  21. 21. Haswellで追加された命令(1/3) 無視されるフラグたち 命令 動作 adcx 符号なし加算(CFのみ変更) adox 符号なし加算(OFのみ変更) mulx 符号なし乗算(フラグ変更なし) sarx 算術右シフト(フラグ変更なし) shlx 論理左シフト(フラグ変更なし) shrx 論理右シフト(フラグ変更なし) rorx 論理右回転(フラグ変更なし) 未定義だった部分が確定した命令 lzcnt bsrの拡張 tzcnt bsfの拡張2013/3/30 #x86opti 5 21 /24
  22. 22. Haswellで追加された命令(2/3) ビット操作系  andn(x, y) = ~x & y 今更感が  bextr(x, start, len) xの[start+len-1:start]のビットを取り出す(範囲外は0拡張)  blsi(x) = x & (-x) 下からみて初めて1となってるビットのみを取り出す  blsr(x) = x & (x-1) 上からみて初めて1となってるビットのみを取り出す  blsmsk(x) = x ^ (x-1) 下からみて初めて1となるところまでのマスクを作る  bzhi(x, n) = x & (~((-1) << n)) nビットより上をクリア2013/3/30 #x86opti 5 22 /24
  23. 23. Haswellで追加された命令(3/3)  pdep(x, mask) maskのビットがたっているところにxのビットを埋め込む x x5 x4 x3 x2 x1 x0 mask 1 1 0 0 1 0 result x2 x1 0 0 x0 0  pext(x, mask) maskのビットが立っているところのxのビットを取り出す x x5 x4 x3 x2 x1 x0 mask 1 1 0 0 1 0 result . . . x5 x4 x12013/3/30 #x86opti 5 23 /24
  24. 24. おまけ 今どきのコンパイラはかしこい ハッシュ関数(fnv-1a)のループ内演算  https://github.com/herumi/misc/blob/master/fnv-1a.cpp for (size_t i = 0; i < n; i++) { v ^= x[i]; v += (v<<1)+(v<<4)+(v<<5)+(v<<7)+(v<<8)+(v<<40); }  「v += シフト&加算」の部分はv *= p(41bitの素数)の形 pのハミング重み(2進数展開の1の数)が小さいものを探して選ぶ  gcc 4.7は素直にleaやaddやshlを組み合わせたコード生成  clang, VCはmulに置き換えた! mov r10, 1099511628211 ; p こっちのほうが2.4倍速い@i7 .lp: movzx r8d, byte [r9+rcx] ハミング重みにこだわらない inc r9 関数探索もありか? xor rax, r8 imul rax, r102013/3/30 #x86opti 5 24 /24

×