Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
SlideShare a Scribd company logo
フラグを愛でる


2013/3/30 光成滋生(@herumi)
x86/x64最適化勉強会5(#x86opti)
目次
 さまざまなコード片でcarryの扱いを味わってみる
   フラグの復習
   絶対値の復習
   ビットの長さ
   ビットカウント
     cybozu/WaveletMatrixの紹介
   多倍長整数加算
   Haswellで追加された命令達

     題材があちこち飛びます m(__)m




2013/3/30 #x86opti 5            2 /24
フラグの復習
 演算ごとに変化する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 ← ecx
2013/3/30 #x86opti 5                                3 /24
絶対値(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
絶対値(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
絶対値(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
ビットの長さ(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
ビットの長さ(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
ビットの長さ(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
ビットの長さ(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
減算のあとの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
減算のあとの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] &= m
2013/3/30 #x86opti 5                                              12 /24
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
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, rax
2013/3/30 #x86opti 5                                       14 /24
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
多倍長整数の加算(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
多倍長整数の加算(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
多倍長整数の加算(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
多倍長整数の加算(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→62clk


2013/3/30 #x86opti 5                            19 /24
多倍長整数の加算(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
Haswellで追加された命令(1/3)
 無視されるフラグたち
                       命令 動作
                       adcx 符号なし加算(CFのみ変更)
                       adox 符号なし加算(OFのみ変更)
                       mulx 符号なし乗算(フラグ変更なし)
                       sarx 算術右シフト(フラグ変更なし)
                       shlx   論理左シフト(フラグ変更なし)
                       shrx   論理右シフト(フラグ変更なし)
                       rorx   論理右回転(フラグ変更なし)
 未定義だった部分が確定した命令
                       lzcnt bsrの拡張
                       tzcnt bsfの拡張
2013/3/30 #x86opti 5                            21 /24
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
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   x1
2013/3/30 #x86opti 5                                 23 /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, r10
2013/3/30 #x86opti 5                                                    24 /24

More Related Content

フラグを愛でる

  • 2. 目次  さまざまなコード片でcarryの扱いを味わってみる  フラグの復習  絶対値の復習  ビットの長さ  ビットカウント cybozu/WaveletMatrixの紹介  多倍長整数加算  Haswellで追加された命令達 題材があちこち飛びます m(__)m 2013/3/30 #x86opti 5 2 /24
  • 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 ← ecx 2013/3/30 #x86opti 5 3 /24
  • 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. 絶対値(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. 絶対値(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. ビットの長さ(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. ビットの長さ(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. ビットの長さ(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. ビットの長さ(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. 減算のあとの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. 減算のあとの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] &= m 2013/3/30 #x86opti 5 12 /24
  • 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. 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, rax 2013/3/30 #x86opti 5 14 /24
  • 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. 多倍長整数の加算(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. 多倍長整数の加算(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. 多倍長整数の加算(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. 多倍長整数の加算(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→62clk 2013/3/30 #x86opti 5 19 /24
  • 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. Haswellで追加された命令(1/3)  無視されるフラグたち 命令 動作 adcx 符号なし加算(CFのみ変更) adox 符号なし加算(OFのみ変更) mulx 符号なし乗算(フラグ変更なし) sarx 算術右シフト(フラグ変更なし) shlx 論理左シフト(フラグ変更なし) shrx 論理右シフト(フラグ変更なし) rorx 論理右回転(フラグ変更なし)  未定義だった部分が確定した命令 lzcnt bsrの拡張 tzcnt bsfの拡張 2013/3/30 #x86opti 5 21 /24
  • 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. 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 x1 2013/3/30 #x86opti 5 23 /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, r10 2013/3/30 #x86opti 5 24 /24