高機能アセンブラによるx86/x64 CPU向
    け高速化テクニック


         Cybozu Labs
       2012/8/24 光成滋生
目次
 x86/x64アセンブラの復習
 C++用のx86/x64アセンブラXbyakの紹介
   サンプル
 文字列検索
 ビットを数える

 自己紹介
  x86/x64最適化勉強会主催
  https://github.com/herumi/opti/
  http://www.slideshare.net/herumi/



夏のプログラムシンポジウム2012                      2 / 33
x86/x64アセンブラの復習(1/2)
 汎用レジスタは15個(64bit時)
  rax, rbx, ..., r8, ..., r15 ; 64bitレジスタ
  rsp : スタックレジスタ
 基本的に2オペランド
  <op> <dst> <src>   //   dst ← op(dst, src);
  dstは破壊される
  アドレッシングは比較的高機能
  ptr [<reg1> + <reg2> * (0|1|2|4|8) + <即値>]

 例)
 mov rax, rcx // rax = rcx;
 add ecx, 4   // ecx += 4;
 sub r8,[rax+ebx*4+12]//r8 -= *(int64_t)(rax+ebx*4+12);
夏のプログラムシンポジウム2012                                         3 / 33
x86/x64アセンブラの復習(2/2)
 SIMDレジスタは16個(64bit時)
  xmm0, ..., xmm15 ; 128bitレジスタ
  ymm0, ..., ymm15 ; 256bitレジスタ
 データ型
  char x 16, int x 4, float x 8, double x 4, etc.
 演算の種類
  四則演算,ビット演算,特殊演算,etc.
  3オペランドタイプもある

 例)
 movaps xmm0,[eax] //xmm0にfloat変数4個が代入される
 vaddpd ymm0,ymm3,ymm2 //ymm0 = ymm3 + ymm2(double x 4)
 pand xmm2, xmm4 //xmm2 &= xmm4
夏のプログラムシンポジウム2012                                    4 / 33
Xbyakの特長
 C++ヘッダのみで記述
  外部ライブラリのビルドやリンクが不要
  対応コンパイラ : Visual Studio/gcc/clang
  対応OS : Windows/Linux/Mac
 実行時コード生成
 Intel MASM形式に似せたDSLを提供
  できるだけ自然に記述できるようにした
  アドレッシングは設計時に一番悩んだ部分
    かつ一番気に入ってる部分

 mov(eax, 5); // mov eax, 5
 add(rcx, byte[eax+ecx*4–5]);//add rcx,byte[eax+ecx*4-5]
 jmp("lp"); // jmp lp
夏のプログラムシンポジウム2012                                      5 / 33
簡単なサンプル(1/3)
 整数nが与えられたときにnを足す関数を生成
  ヘッダをincludeしてCodeGeneratorを継承

 #include <xbyak.h>

 struct Code : Xbyak::CodeGenerator {
   explicit Code(int n) {
     mov(eax, ptr [esp + 4]); // 32bit OS
     add(eax, n);
     // lea(rax, ptr [rcx + n]); // 64bit Windows
     // lea(eax, ptr [edi + n]); // 64bit Linux/Mac
     ret();
   }
 };

夏のプログラムシンポジウム2012                                     6 / 33
簡単なサンプル(2/3)
 インスタンスを生成して実行
 int main(int argc, char *argv[]) {
   const int n = argc == 1 ? 5 : atoi(argv[1]);
   Code c(n);
   auto f = (int (*)(int))c.getCode();
   for (int i = 0; i < 3; i++) {
     printf("%d + %d = %dn", i, n, f(i));
   }
 }

 %   ./a.out 9
 0   + 9 = 9
 1   + 9 = 10
 2   + 9 = 11
夏のプログラムシンポジウム2012                                 7 / 33
簡単なサンプル(3/3)
 インスタンスを生成して実行
  引数が9のときの関数fの中身をデバッガで確認
  32bit Windowsで見てみた
 mov eax,dword ptr [esp+4]
 add eax,9 // ここが即値
 ret
  引数が3のときの関数fの中身をデバッガで確認
  64bit Linuxで見てみた

 0x0000000000607000 in ?? ()
 1: x/i $pc
   0x607000:    lea    eax,[edi+0x3]
                                 // ここが即値

夏のプログラムシンポジウム2012                           8 / 33
応用例
 ビューティフルコード8章
  画像処理のためのその場コード生成
  画像処理では関数内では固定のパラメータが多い
  templateなどを組み合わせるとバイナリサイズが肥大化




夏のプログラムシンポジウム2012                  9 / 33
画像処理のためのその場コード生成
 ビットマップD, Sを合成変換する関数
  変換種類を示すopがループの最内部分にあるため通常の実装
   では遅すぎる
  1985年のWindowsではそのコードを生成するミニコンパイラ
   を含んでいたらしい

 for (int y = 0; y < cy; y++) {
   for (int x = 0; x < cx; x++) {
     switch (op) {
     case 0x00: D[y][x] = 0; break;
     ...
     case 0x60: D[y][x] = (D[y][x] ^ S[y][x]) & P[y][x];
     ...
     } } }
夏のプログラムシンポジウム2012                                     10 / 33
Xbyakによるその場コード生成
 メリット                     Code(const Rect& rect, int op){
                              // generate prolog
  C++の文法でasmの制御構造を L(".lp_y");
   記述できる                      mov(rcx, rect.width);
                           L(".lp_x");
  極めて直感的に扱える
                            switch (op) {
  アセンブラ独自の疑似命令を              ..
    覚える必要がない                  case 0x60:
                                mov(eax,ptr[ptrD+rbx]);
  Cの構造体との連携が                   xor(eax,ptr[ptrS+rbx]);
   シームレス                        and(eax,ptr[ptrP+rbx]);
  構造体メンバのオフセット                 break;
    取得は外部ツールでは困難            }
                            mov(ptr[ptrD + rbx], eax);
  Xbyakなら<stddef.h>の       add(rbx, 4);
    offsetof(type, member)  sub(rcx, 1);
    をそのまま利用可能               jnz(".lp_x");
                              ...

夏のプログラムシンポジウム2012                                      11 / 33
FFTのバタフライ演算(1/2)
 ビット反転とデータ移動    void swap(double*a,int k1,int j1){
                  __m128d x = _mm_load_pd(a + j1);
  入力は次数nとデータa    __m128d y = _mm_load_pd(a + k1);
  ipはビット反転用work  __mm_store_pd(a + j1, y);
                  __mm_store_pd(a + k1, x);
  nは可変だが,128とか  }
   256とかが多いケース   void bitrv(int n,int *ip,double *a)
  通常は事前に専用コード   {
                   int j, j1, k, k1, l, m, m2;
   を用意して分岐する
                   l = n; m = 1;
  ツールでCのコードを生成    ...
    するなど           m2 = 2 * m;
                   if ((m << 3) == l) {
                       for (k = 0; k < m; k++) {
                           for (j = 0; j < k; j++) {
                               j1 = 2 * j + ip[k];
                               k1 = 2 * k + ip[j];
                               swap(a, j1, k1);
夏のプログラムシンポジウム2012                               12 / 33
FFTのバタフライ演算(2/2)
 修正箇所はわずか                    struct Code : Xbyak::CodeGenerator {
  swap()をコード生成                 void swap(const Reg32e& a
                                  , int k1, int j1){
   するように変更                        movapd(xm0, ptr [a + j1 * 8]);
  関数のプロローグと                      movapd(xm1, ptr [a + k1 * 8]);
   エピローグを作る                       movapd(ptr [a + j1 * 8], xm1);
                                  movapd(ptr [a + k1 * 8], xm0);
  生成コード例                       }
  定数とループが全て                    void gen_bitrv2(int n, int *ip){
    展開される                         const Reg64& a = rdi;
                                  int j, j1, k, k1, l, m, m2;
movapd   xm0,ptr [eax+10h]        ...
movapd   xm1,ptr [eax+100h]             j1 = 2 * j + ip[k];
movapd   ptr [eax+10h],xm1              k1 = 2 * k + ip[j];
movapd   ptr [eax+100h],xm0             swap(a, j1, k1);
movapd   xm0,ptr [eax+50h]              ...
movapd   xm1,ptr [eax+140h]
...
夏のプログラムシンポジウム2012                                             13 / 33
SSE4.1の文字列探索命令の紹介
 pcmpestri, pcmpistriなど
  strlenやstrstrなどを高速に実装するための命令群
 複雑なパラメータを持つCISCの権化の様な命令
  文字列処理の単位char or short(2byte:UTF16)の選択
  符号付き・符号無しの選択
  文字列の比較方法の選択
  完全マッチ,文字の範囲指定,部分文字列など
  入力文字列の設定
  0ターミネート文字列 or [begin, end)による文字列
  CF, ZF, SF, OFの各フラグに結果の様々な情報
 詳細
  http://www.slideshare.net/herumi/x86opti3

夏のプログラムシンポジウム2012                              14 / 33
strstrを作ってみる
 16byteずつ文字列を探索する
  入力パラメータ
  a : 入力テキスト(text)ポインタが格納されたレジスタ
  xm0 : 検索文字(の先頭最大16byte)
  12 : 文字列マッチをuint8_tの部分文字マッチさせる即値
  出力フラグ
  CF : 文字列を見つける前にtextが終われば1
  ZF : aから16byte内に文字列がなければ0
  movdqu(xm0, ptr [key]); // xm0 = *key
L(".lp");
  pcmpistri(xm0, ptr [a], 12);
  lea(a, ptr [a + 16]);
  ja(".lp");
  jnc(".notFound");
夏のプログラムシンポジウム2012                         15 / 33
strstrのコア部分(続き)
 keyの先頭文字が一致する部分を検出した
  先程のループを抜けたときはcにマッチしたオフセットが入っ
   ているのでそれだけ進める(add a, c)
  その場所からkeyの最後まで一致しているかを確認
  OF == 0なら一致しなかった
  SF == 1なら見つかった
  add(a, c);
  mov(save_a, a); // save a
  mov(save_key, key); // save key
L(".tailCmp");
  movdqu(xm1, ptr [save_key]);
  pcmpistri(xm1, ptr [save_a], 12);
  jno(".next"); // if (OF == 0) goto .next
  js(".found"); // if (SF == 1) goto .found
  add(save_a, 16);
  add(save_key, 16);
  jmp(".tailCmp");
夏のプログラムシンポジウム2012                             16 / 33
ベンチマーク
 対象CPU, コンパイラ
  Xeon X5650 + gcc 4.6.3
 対象コード
  gccのstrstr(これもSSE4.1を利用している)
  boost 1.51のalgorithm::boyer_moore(BM法)
  Quick Search(改良版BMアルゴリズム)
  my_strstr
 検索方法
  テキスト:130MBのUTF8な日本語を多く含むもの
  指定されたkey文字列がいくつあるかを探す
  byte単位あたりにかかったCPU clock数を表示する


夏のプログラムシンポジウム2012                           17 / 33
strstrのベンチマーク
 Xeon X5650 + gcc 4.6.3
                             8                                               string::find
                             7                                               boost::bm
                                                                                             fast
cycle/Byte to find




                             6                                               quick search
                             5                                               strstr(gcc)
                             4                                               my_strstr
                             3
                             2
                             1
                             0
                                                                static_ass
                                     a     ab     1234   これは                   00...0       AB...Z
                                                                    ert
                     string::find   3.37   3.04   3.23   7.39     2.95          3.27        2.8
                     boost::bm      6.76   6.27   3.23   1.74     1.07          0.93        0.56
                     quick search   4.7    3.12   1.99   3.4      0.85          0.7         0.54
                     strstr(gcc)    1.64   1.13   1.12   1.15     1.11          1.18        0.46
                     my_strstr      0.6    0.3    0.29   0.8      0.28          0.3         0.27
夏のプログラムシンポジウム2012                                                                             18 / 33
考察
 ABC....Zの様なBM法やQuick Searchアルゴリズムにと
  って有利な文字列すらstrstrより遅い
  BM法やQuick Searchはテーブルを引いてオフセットを足すた
   めパイプラインに悪影響がある
  通常の文字列検索では出番がない?
 // Quick Searchの検索部分
 const char *find(const char *begin, const char *end) const {
     while (begin <= end - len_) {
       for (size_t i = 0; i < len_; i++) {
         if (str_[i] != begin[i]) goto NEXT;
       }
       return begin;
     NEXT:
       begin += tbl_[static_cast<unsigned char>(begin[len_])];
     }
     return end;
   }
夏のプログラムシンポジウム2012                                                19 / 33
strcasestrの実装
 keyの大文字小文字を区別しないstrstr
  コードの簡略さのためにkeyは事前に小文字化しておく
 大文字小文字を区別しないマッチ方法
  textを小文字にしてマッチさせる(先程のコードに委譲)
  movdqu(xm0, ptr [key]); // xm0 = *key
L(".lp");
  if (caseInsensitive) {
    movdqu(xm1, ptr [a]);
    toLower(xm1, Am1, Zp1, amA, t0, t1);//小文字化するコード生成関数
    pcmpistri(xm0, xm1, 12);
  } else {
    pcmpistri(xm0, ptr [a], 12);
  }
  lea(a, ptr [a + 16]);
  ja(".lp"); // if (CF == 0 and ZF = 0) goto .lp
  jnc(".notFound");
夏のプログラムシンポジウム2012                                   20 / 33
toLower(1/2)
 大文字を小文字にする
  'A' <= c && c <= 'Z'なら c += 'a' – 'Z'
  分岐無しで16byteずつまとめてやりたい
 pcmpgtb x, y
  x ← x > y ? 0xff : 0;をbyte単位で行う関数
  if ('A' <= c && c <= 'Z') c += 'a' – 'Z';を書き換える
  ('A' <= c && c <= 'Z') ? ('a' – 'Z') : 0;
= ((c > 'A'–1) ? 0xff : 0) & (('Z'+1 > c) ? 0xff : 0) & ('a'–'Z');
= pcmpgtb(c, 'A'-1) & pcmpgtb('Z'+1, c) & 'a'-'Z';




夏のプログラムシンポジウム2012                                               21 / 33
toLower(2/2)
 実際のコード片
/*
     toLower in x
     Am1 : 'A' – 1, Zp1 : 'Z' + 1, amA : 'a' - 'A'
     t0, t1 : temporary register
*/
void toLower(const Xmm& x, const Xmm& Am1, const Xmm& Zp1
    , const Xmm& amA , const Xmm& t0, const Xmm& t1) {
   movdqa(t0, x);
   pcmpgtb(t0, Am1); // -1 if c > 'A' - 1
   movdqa(t1, Zp1);
   pcmpgtb(t1, x); // -1 if 'Z' + 1 > c
   pand(t0, t1); // -1 if [A-Z]
   pand(t0, amA); // 0x20 if c in [A-Z]
   paddb(x, t0); // [A-Z] -> [a-z]
}


夏のプログラムシンポジウム2012                                           22 / 33
CPU判別によるディスパッチ
 コア部分再度(詳細)        pcmpistri(xm0,ptr[a],12);
  実験によるとCPUの世代で     if (isSandyBridge) {
   コードの書き方で速度が違う       lea(a, ptr [a + 16]);
   (lea vs add)        ja(".lp");
                     } else {
  実行時にCPU判別することで
                       jbe(".headCmp");
   適切なコード生成を行う
                       add(a, 16);
  10%程度速度向上があった       jmp(".lp");
                    L(".headCmp");
                     }
                     jnc(".notFound");
                     if (isSandyBridge) {
                        lea(a,ptr[a+c-16]);
                     } else {
                         add(a, c); }
夏のプログラムシンポジウム2012                         23 / 33
ビットを数える
 簡潔データ構造
  サイズnの{0, 1}からなるビットベクトルvに対して
   rank(x) = #{ v[i] = 1 | 0 <= i < x }
   select(m) = min { i | rank(i) = m }
   を基本関数として圧縮検索などさまざまなロジックに利用
  (注)普通はrank()は0 <= i <= xの範囲で定義
  rankはまさにビットカウント関数
  ビューティフルコード10章
    「高速ビットカウントを求めて」の続き?
  ここでは32bit/64bitに対するビットカウント命令
    popcntの存在は前提




夏のプログラムシンポジウム2012                     24 / 33
ナイーブな実装
 岡野原さんの2006年CodeZine記事ベース
  256bitごとの累積値をuint32_t a[]に保存
  a[i] := rank(256 * i)
  そこからの64bitごとの差分累積値をuint8_t b[]に保存
  b[i] := rank(64 * i) – rank(64 * (i & ~3))
  必要なメモリは(32 + 8 * 4) / 256 = 1/4(bitあたり)
  rank(x) = a[i/256] + b[i/64]+ popcnt(v[i/64] & mask);
  ランダムアクセスするのでキャッシュミスが頻出
          .    .                 .   .           .         .         .          .
0 1 1 0          0 1   0 1 1 0         0 1 ... 0   1   0     1   1     1   1      0
          .    .                 .   .           .         .         .          .
              256bit                              64bit
      a[0]                   a[1]             b[x+0]   b[x+1]    b[x+2]        b[x+3]


夏のプログラムシンポジウム2012                                                               25 / 33
メモリを減らして高速化できる?
 キャッシュ効率の向上
  テーブルは一つにまとめる
 256bit単位ではなく512bit単位で集めてみる
  64bitで区切ると8個
  するとメモリ必要量は(32 + 32 + 8 * 8) / 512 = 1/4
  変わらない
  128bitで区切ると4個
  メモリ必要量は(32 + 8 * 4) / 512 = 1/8
  半分になる
  問題点
  256bitを超えるのでuint8_tな配列b[]の積算で
    オーバーフローしてしまう
     オンデマンドで総和を求める?
夏のプログラムシンポジウム2012                            26 / 33
4個未満のsum()
 最適化したいコード(0 <= n < 4)
  Xeonで35clk, i7で30lk程度であった(乱数生成込み)
int sum1(const uint8_t data[4], int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += data[i];
    }
    return sum;
}



  XorShift128 rg;
  for (int j = 0; j < C; j++) {
     ret += sum1(data, rg.get() % 4);
  }

夏のプログラムシンポジウム2012                          27 / 33
ループ展開してみた
int sum2(const uint8_t data[4], int n) {
    int sum = 0;
    switch (n) {
    case 3: sum += data[2];
    case 2: sum += data[1];
    case 1: sum += data[0];
    }
    return sum;
}


 Xeon 35→26clk, i7 30→24clk
  Loop Stream Detector(LSD)




夏のプログラムシンポジウム2012                          28 / 33
psadbwを使ってみる
 psadbw X, Y
  MPEGなどのビデオコーデックにおいて
   二つのbyte単位のデータの差の絶対値の和を求める命令
  psadbw(X, Y) = sum [abs(X[i] – Y[i]) | i = 0..7]
  画像の一致度を求める
  Y = 0とするとXのbyte単位の和を求められる
  Xをマスクして足せばよい
   x0     x1     x2    x3    x4   x5   x6   x7
                       and
  0xff   0xff   0xff    0    0    0    0    0


  x0     x1     x2     0     0    0    0    0

夏のプログラムシンポジウム2012
                       x0 + x1 + x2                   29 / 33
実際のコード
int sum3(const uint8_t data[4], int n) {
  uint32_t x = *reinterpret_cast<const uint32_t*>(data);
  x &= (1U << (n * 8)) - 1;
  V128 v(x);
  v = psadbw(v, Zero());
  return movd(v);
}

 Xeon 35→26→10clk!
  i7     30→24→10clk!
  速くなった
  このコードはn < 8まで適用可能
  ちょっと改良すればn < 16まで可能



夏のプログラムシンポジウム2012                                          30 / 33
128bitマスク
 残りはマスクしてpopcntする部分
// 疑似コード
uint128_t mask128(int n) {
  return (uint128_t(1) << n) – 1; }

int get(uint12_t x, int n) {
  return popcnt_128(x & mask128(n)); }

  実際には64bit整数に分解して実行する
  n < 63 と n >= 64に場合分けする
uint64_t m0 = -1;
uint64_t m1 = 0;
if (!(n > 64)) m0 = mask;
if (n > 64) m1 = mask;
ret += popCount64(b0 & m0);
ret += popCount64(b1 & m1);

夏のプログラムシンポジウム2012                        31 / 33
分岐除去
 先程のコードはgcc4.6では条件分岐命令を生成する
  データがランダムなら確率50%で分岐予測が外れる
  cmovを使って実装
  条件が成立したときのみmovを行う命令
  if (n > 64)はcarryを変更しなければ一度だけでよい
  6clkぐらい速くなる
     メモリアクセスの影響が多いと見えにくくなる
or(m0, -1);
and(n, 64); // ZF = (n < 64) ? 1 : 0
cmovz(m0, mask); // m0 = (!(n > 64)) ? mask : -1
cmovz(mask, idx); // mask = (n > 64) ? mask : 0
and(m0, ptr [blk + rax * 8 + 0]);
and(mask, ptr [blk + rax * 8 + 8]);
popcnt(m0, m0);
popcnt(rax, mask);
夏のプログラムシンポジウム2012                                  32 / 33
select1のベンチマーク
 メモリを大量に使うところでは速い
  marisa-trieのbit-vectorよりも速い
  少ないところではオーバーヘッドがややある
  まだ改良の余地あり・あるいはnによって戦略を変える
                           rankの処理時間(clk)                           fast
  200

  150

  100

   50

    0
        128KiB 0.5MiB   2MiB   8MiB   32MiB 0.1GiB 0.5GiB   2GiB   4GiB
                               SBV1(org)   SBV2

夏のプログラムシンポジウム2012                                                         33 / 33

Prosym2012

  • 1.
    高機能アセンブラによるx86/x64 CPU向 け高速化テクニック Cybozu Labs 2012/8/24 光成滋生
  • 2.
    目次  x86/x64アセンブラの復習  C++用のx86/x64アセンブラXbyakの紹介 サンプル  文字列検索  ビットを数える  自己紹介  x86/x64最適化勉強会主催  https://github.com/herumi/opti/  http://www.slideshare.net/herumi/ 夏のプログラムシンポジウム2012 2 / 33
  • 3.
    x86/x64アセンブラの復習(1/2)  汎用レジスタは15個(64bit時) rax, rbx, ..., r8, ..., r15 ; 64bitレジスタ  rsp : スタックレジスタ  基本的に2オペランド  <op> <dst> <src> // dst ← op(dst, src); dstは破壊される  アドレッシングは比較的高機能 ptr [<reg1> + <reg2> * (0|1|2|4|8) + <即値>] 例) mov rax, rcx // rax = rcx; add ecx, 4 // ecx += 4; sub r8,[rax+ebx*4+12]//r8 -= *(int64_t)(rax+ebx*4+12); 夏のプログラムシンポジウム2012 3 / 33
  • 4.
    x86/x64アセンブラの復習(2/2)  SIMDレジスタは16個(64bit時) xmm0, ..., xmm15 ; 128bitレジスタ  ymm0, ..., ymm15 ; 256bitレジスタ  データ型  char x 16, int x 4, float x 8, double x 4, etc.  演算の種類  四則演算,ビット演算,特殊演算,etc. 3オペランドタイプもある 例) movaps xmm0,[eax] //xmm0にfloat変数4個が代入される vaddpd ymm0,ymm3,ymm2 //ymm0 = ymm3 + ymm2(double x 4) pand xmm2, xmm4 //xmm2 &= xmm4 夏のプログラムシンポジウム2012 4 / 33
  • 5.
    Xbyakの特長  C++ヘッダのみで記述 外部ライブラリのビルドやリンクが不要 対応コンパイラ : Visual Studio/gcc/clang 対応OS : Windows/Linux/Mac  実行時コード生成  Intel MASM形式に似せたDSLを提供  できるだけ自然に記述できるようにした アドレッシングは設計時に一番悩んだ部分  かつ一番気に入ってる部分 mov(eax, 5); // mov eax, 5 add(rcx, byte[eax+ecx*4–5]);//add rcx,byte[eax+ecx*4-5] jmp("lp"); // jmp lp 夏のプログラムシンポジウム2012 5 / 33
  • 6.
    簡単なサンプル(1/3)  整数nが与えられたときにnを足す関数を生成 ヘッダをincludeしてCodeGeneratorを継承 #include <xbyak.h> struct Code : Xbyak::CodeGenerator { explicit Code(int n) { mov(eax, ptr [esp + 4]); // 32bit OS add(eax, n); // lea(rax, ptr [rcx + n]); // 64bit Windows // lea(eax, ptr [edi + n]); // 64bit Linux/Mac ret(); } }; 夏のプログラムシンポジウム2012 6 / 33
  • 7.
    簡単なサンプル(2/3)  インスタンスを生成して実行 intmain(int argc, char *argv[]) { const int n = argc == 1 ? 5 : atoi(argv[1]); Code c(n); auto f = (int (*)(int))c.getCode(); for (int i = 0; i < 3; i++) { printf("%d + %d = %dn", i, n, f(i)); } } % ./a.out 9 0 + 9 = 9 1 + 9 = 10 2 + 9 = 11 夏のプログラムシンポジウム2012 7 / 33
  • 8.
    簡単なサンプル(3/3)  インスタンスを生成して実行 引数が9のときの関数fの中身をデバッガで確認 32bit Windowsで見てみた mov eax,dword ptr [esp+4] add eax,9 // ここが即値 ret  引数が3のときの関数fの中身をデバッガで確認 64bit Linuxで見てみた 0x0000000000607000 in ?? () 1: x/i $pc 0x607000: lea eax,[edi+0x3] // ここが即値 夏のプログラムシンポジウム2012 8 / 33
  • 9.
    応用例  ビューティフルコード8章 画像処理のためのその場コード生成  画像処理では関数内では固定のパラメータが多い templateなどを組み合わせるとバイナリサイズが肥大化 夏のプログラムシンポジウム2012 9 / 33
  • 10.
    画像処理のためのその場コード生成  ビットマップD, Sを合成変換する関数  変換種類を示すopがループの最内部分にあるため通常の実装 では遅すぎる 1985年のWindowsではそのコードを生成するミニコンパイラ を含んでいたらしい for (int y = 0; y < cy; y++) { for (int x = 0; x < cx; x++) { switch (op) { case 0x00: D[y][x] = 0; break; ... case 0x60: D[y][x] = (D[y][x] ^ S[y][x]) & P[y][x]; ... } } } 夏のプログラムシンポジウム2012 10 / 33
  • 11.
    Xbyakによるその場コード生成  メリット Code(const Rect& rect, int op){ // generate prolog  C++の文法でasmの制御構造を L(".lp_y"); 記述できる mov(rcx, rect.width); L(".lp_x"); 極めて直感的に扱える switch (op) { アセンブラ独自の疑似命令を .. 覚える必要がない case 0x60: mov(eax,ptr[ptrD+rbx]);  Cの構造体との連携が xor(eax,ptr[ptrS+rbx]); シームレス and(eax,ptr[ptrP+rbx]); 構造体メンバのオフセット break; 取得は外部ツールでは困難 } mov(ptr[ptrD + rbx], eax); Xbyakなら<stddef.h>の add(rbx, 4); offsetof(type, member) sub(rcx, 1); をそのまま利用可能 jnz(".lp_x"); ... 夏のプログラムシンポジウム2012 11 / 33
  • 12.
    FFTのバタフライ演算(1/2)  ビット反転とデータ移動 void swap(double*a,int k1,int j1){ __m128d x = _mm_load_pd(a + j1);  入力は次数nとデータa __m128d y = _mm_load_pd(a + k1); ipはビット反転用work __mm_store_pd(a + j1, y); __mm_store_pd(a + k1, x);  nは可変だが,128とか } 256とかが多いケース void bitrv(int n,int *ip,double *a)  通常は事前に専用コード { int j, j1, k, k1, l, m, m2; を用意して分岐する l = n; m = 1; ツールでCのコードを生成 ... するなど m2 = 2 * m; if ((m << 3) == l) { for (k = 0; k < m; k++) { for (j = 0; j < k; j++) { j1 = 2 * j + ip[k]; k1 = 2 * k + ip[j]; swap(a, j1, k1); 夏のプログラムシンポジウム2012 12 / 33
  • 13.
    FFTのバタフライ演算(2/2)  修正箇所はわずか struct Code : Xbyak::CodeGenerator {  swap()をコード生成 void swap(const Reg32e& a , int k1, int j1){ するように変更 movapd(xm0, ptr [a + j1 * 8]);  関数のプロローグと movapd(xm1, ptr [a + k1 * 8]); エピローグを作る movapd(ptr [a + j1 * 8], xm1); movapd(ptr [a + k1 * 8], xm0);  生成コード例 } 定数とループが全て void gen_bitrv2(int n, int *ip){ 展開される const Reg64& a = rdi; int j, j1, k, k1, l, m, m2; movapd xm0,ptr [eax+10h] ... movapd xm1,ptr [eax+100h] j1 = 2 * j + ip[k]; movapd ptr [eax+10h],xm1 k1 = 2 * k + ip[j]; movapd ptr [eax+100h],xm0 swap(a, j1, k1); movapd xm0,ptr [eax+50h] ... movapd xm1,ptr [eax+140h] ... 夏のプログラムシンポジウム2012 13 / 33
  • 14.
    SSE4.1の文字列探索命令の紹介  pcmpestri, pcmpistriなど  strlenやstrstrなどを高速に実装するための命令群  複雑なパラメータを持つCISCの権化の様な命令  文字列処理の単位char or short(2byte:UTF16)の選択  符号付き・符号無しの選択  文字列の比較方法の選択 完全マッチ,文字の範囲指定,部分文字列など  入力文字列の設定 0ターミネート文字列 or [begin, end)による文字列  CF, ZF, SF, OFの各フラグに結果の様々な情報  詳細  http://www.slideshare.net/herumi/x86opti3 夏のプログラムシンポジウム2012 14 / 33
  • 15.
    strstrを作ってみる  16byteずつ文字列を探索する 入力パラメータ a : 入力テキスト(text)ポインタが格納されたレジスタ xm0 : 検索文字(の先頭最大16byte) 12 : 文字列マッチをuint8_tの部分文字マッチさせる即値  出力フラグ CF : 文字列を見つける前にtextが終われば1 ZF : aから16byte内に文字列がなければ0 movdqu(xm0, ptr [key]); // xm0 = *key L(".lp"); pcmpistri(xm0, ptr [a], 12); lea(a, ptr [a + 16]); ja(".lp"); jnc(".notFound"); 夏のプログラムシンポジウム2012 15 / 33
  • 16.
    strstrのコア部分(続き)  keyの先頭文字が一致する部分を検出した 先程のループを抜けたときはcにマッチしたオフセットが入っ ているのでそれだけ進める(add a, c)  その場所からkeyの最後まで一致しているかを確認 OF == 0なら一致しなかった SF == 1なら見つかった add(a, c); mov(save_a, a); // save a mov(save_key, key); // save key L(".tailCmp"); movdqu(xm1, ptr [save_key]); pcmpistri(xm1, ptr [save_a], 12); jno(".next"); // if (OF == 0) goto .next js(".found"); // if (SF == 1) goto .found add(save_a, 16); add(save_key, 16); jmp(".tailCmp"); 夏のプログラムシンポジウム2012 16 / 33
  • 17.
    ベンチマーク  対象CPU, コンパイラ  Xeon X5650 + gcc 4.6.3  対象コード  gccのstrstr(これもSSE4.1を利用している)  boost 1.51のalgorithm::boyer_moore(BM法)  Quick Search(改良版BMアルゴリズム)  my_strstr  検索方法  テキスト:130MBのUTF8な日本語を多く含むもの 指定されたkey文字列がいくつあるかを探す byte単位あたりにかかったCPU clock数を表示する 夏のプログラムシンポジウム2012 17 / 33
  • 18.
    strstrのベンチマーク  Xeon X5650+ gcc 4.6.3 8 string::find 7 boost::bm fast cycle/Byte to find 6 quick search 5 strstr(gcc) 4 my_strstr 3 2 1 0 static_ass a ab 1234 これは 00...0 AB...Z ert string::find 3.37 3.04 3.23 7.39 2.95 3.27 2.8 boost::bm 6.76 6.27 3.23 1.74 1.07 0.93 0.56 quick search 4.7 3.12 1.99 3.4 0.85 0.7 0.54 strstr(gcc) 1.64 1.13 1.12 1.15 1.11 1.18 0.46 my_strstr 0.6 0.3 0.29 0.8 0.28 0.3 0.27 夏のプログラムシンポジウム2012 18 / 33
  • 19.
    考察  ABC....Zの様なBM法やQuick Searchアルゴリズムにと って有利な文字列すらstrstrより遅い  BM法やQuick Searchはテーブルを引いてオフセットを足すた めパイプラインに悪影響がある  通常の文字列検索では出番がない? // Quick Searchの検索部分 const char *find(const char *begin, const char *end) const { while (begin <= end - len_) { for (size_t i = 0; i < len_; i++) { if (str_[i] != begin[i]) goto NEXT; } return begin; NEXT: begin += tbl_[static_cast<unsigned char>(begin[len_])]; } return end; } 夏のプログラムシンポジウム2012 19 / 33
  • 20.
    strcasestrの実装  keyの大文字小文字を区別しないstrstr コードの簡略さのためにkeyは事前に小文字化しておく  大文字小文字を区別しないマッチ方法  textを小文字にしてマッチさせる(先程のコードに委譲) movdqu(xm0, ptr [key]); // xm0 = *key L(".lp"); if (caseInsensitive) { movdqu(xm1, ptr [a]); toLower(xm1, Am1, Zp1, amA, t0, t1);//小文字化するコード生成関数 pcmpistri(xm0, xm1, 12); } else { pcmpistri(xm0, ptr [a], 12); } lea(a, ptr [a + 16]); ja(".lp"); // if (CF == 0 and ZF = 0) goto .lp jnc(".notFound"); 夏のプログラムシンポジウム2012 20 / 33
  • 21.
    toLower(1/2)  大文字を小文字にする 'A' <= c && c <= 'Z'なら c += 'a' – 'Z'  分岐無しで16byteずつまとめてやりたい  pcmpgtb x, y  x ← x > y ? 0xff : 0;をbyte単位で行う関数  if ('A' <= c && c <= 'Z') c += 'a' – 'Z';を書き換える ('A' <= c && c <= 'Z') ? ('a' – 'Z') : 0; = ((c > 'A'–1) ? 0xff : 0) & (('Z'+1 > c) ? 0xff : 0) & ('a'–'Z'); = pcmpgtb(c, 'A'-1) & pcmpgtb('Z'+1, c) & 'a'-'Z'; 夏のプログラムシンポジウム2012 21 / 33
  • 22.
    toLower(2/2)  実際のコード片 /* toLower in x Am1 : 'A' – 1, Zp1 : 'Z' + 1, amA : 'a' - 'A' t0, t1 : temporary register */ void toLower(const Xmm& x, const Xmm& Am1, const Xmm& Zp1 , const Xmm& amA , const Xmm& t0, const Xmm& t1) { movdqa(t0, x); pcmpgtb(t0, Am1); // -1 if c > 'A' - 1 movdqa(t1, Zp1); pcmpgtb(t1, x); // -1 if 'Z' + 1 > c pand(t0, t1); // -1 if [A-Z] pand(t0, amA); // 0x20 if c in [A-Z] paddb(x, t0); // [A-Z] -> [a-z] } 夏のプログラムシンポジウム2012 22 / 33
  • 23.
    CPU判別によるディスパッチ  コア部分再度(詳細) pcmpistri(xm0,ptr[a],12);  実験によるとCPUの世代で if (isSandyBridge) { コードの書き方で速度が違う lea(a, ptr [a + 16]); (lea vs add) ja(".lp"); } else {  実行時にCPU判別することで jbe(".headCmp"); 適切なコード生成を行う add(a, 16);  10%程度速度向上があった jmp(".lp"); L(".headCmp"); } jnc(".notFound"); if (isSandyBridge) { lea(a,ptr[a+c-16]); } else { add(a, c); } 夏のプログラムシンポジウム2012 23 / 33
  • 24.
    ビットを数える  簡潔データ構造 サイズnの{0, 1}からなるビットベクトルvに対して rank(x) = #{ v[i] = 1 | 0 <= i < x } select(m) = min { i | rank(i) = m } を基本関数として圧縮検索などさまざまなロジックに利用 (注)普通はrank()は0 <= i <= xの範囲で定義  rankはまさにビットカウント関数 ビューティフルコード10章 「高速ビットカウントを求めて」の続き? ここでは32bit/64bitに対するビットカウント命令 popcntの存在は前提 夏のプログラムシンポジウム2012 24 / 33
  • 25.
    ナイーブな実装  岡野原さんの2006年CodeZine記事ベース 256bitごとの累積値をuint32_t a[]に保存 a[i] := rank(256 * i)  そこからの64bitごとの差分累積値をuint8_t b[]に保存 b[i] := rank(64 * i) – rank(64 * (i & ~3))  必要なメモリは(32 + 8 * 4) / 256 = 1/4(bitあたり)  rank(x) = a[i/256] + b[i/64]+ popcnt(v[i/64] & mask); ランダムアクセスするのでキャッシュミスが頻出 . . . . . . . . 0 1 1 0 0 1 0 1 1 0 0 1 ... 0 1 0 1 1 1 1 0 . . . . . . . . 256bit 64bit a[0] a[1] b[x+0] b[x+1] b[x+2] b[x+3] 夏のプログラムシンポジウム2012 25 / 33
  • 26.
    メモリを減らして高速化できる?  キャッシュ効率の向上 テーブルは一つにまとめる  256bit単位ではなく512bit単位で集めてみる  64bitで区切ると8個  するとメモリ必要量は(32 + 32 + 8 * 8) / 512 = 1/4 変わらない  128bitで区切ると4個  メモリ必要量は(32 + 8 * 4) / 512 = 1/8 半分になる  問題点 256bitを超えるのでuint8_tな配列b[]の積算で オーバーフローしてしまう  オンデマンドで総和を求める? 夏のプログラムシンポジウム2012 26 / 33
  • 27.
    4個未満のsum()  最適化したいコード(0 <=n < 4)  Xeonで35clk, i7で30lk程度であった(乱数生成込み) int sum1(const uint8_t data[4], int n) { int sum = 0; for (int i = 0; i < n; i++) { sum += data[i]; } return sum; } XorShift128 rg; for (int j = 0; j < C; j++) { ret += sum1(data, rg.get() % 4); } 夏のプログラムシンポジウム2012 27 / 33
  • 28.
    ループ展開してみた int sum2(const uint8_tdata[4], int n) { int sum = 0; switch (n) { case 3: sum += data[2]; case 2: sum += data[1]; case 1: sum += data[0]; } return sum; }  Xeon 35→26clk, i7 30→24clk  Loop Stream Detector(LSD) 夏のプログラムシンポジウム2012 28 / 33
  • 29.
    psadbwを使ってみる  psadbw X,Y  MPEGなどのビデオコーデックにおいて 二つのbyte単位のデータの差の絶対値の和を求める命令  psadbw(X, Y) = sum [abs(X[i] – Y[i]) | i = 0..7] 画像の一致度を求める  Y = 0とするとXのbyte単位の和を求められる  Xをマスクして足せばよい x0 x1 x2 x3 x4 x5 x6 x7 and 0xff 0xff 0xff 0 0 0 0 0 x0 x1 x2 0 0 0 0 0 夏のプログラムシンポジウム2012 x0 + x1 + x2 29 / 33
  • 30.
    実際のコード int sum3(const uint8_tdata[4], int n) { uint32_t x = *reinterpret_cast<const uint32_t*>(data); x &= (1U << (n * 8)) - 1; V128 v(x); v = psadbw(v, Zero()); return movd(v); }  Xeon 35→26→10clk! i7 30→24→10clk!  速くなった  このコードはn < 8まで適用可能 ちょっと改良すればn < 16まで可能 夏のプログラムシンポジウム2012 30 / 33
  • 31.
    128bitマスク  残りはマスクしてpopcntする部分 // 疑似コード uint128_tmask128(int n) { return (uint128_t(1) << n) – 1; } int get(uint12_t x, int n) { return popcnt_128(x & mask128(n)); }  実際には64bit整数に分解して実行する n < 63 と n >= 64に場合分けする uint64_t m0 = -1; uint64_t m1 = 0; if (!(n > 64)) m0 = mask; if (n > 64) m1 = mask; ret += popCount64(b0 & m0); ret += popCount64(b1 & m1); 夏のプログラムシンポジウム2012 31 / 33
  • 32.
    分岐除去  先程のコードはgcc4.6では条件分岐命令を生成する データがランダムなら確率50%で分岐予測が外れる  cmovを使って実装 条件が成立したときのみmovを行う命令 if (n > 64)はcarryを変更しなければ一度だけでよい 6clkぐらい速くなる  メモリアクセスの影響が多いと見えにくくなる or(m0, -1); and(n, 64); // ZF = (n < 64) ? 1 : 0 cmovz(m0, mask); // m0 = (!(n > 64)) ? mask : -1 cmovz(mask, idx); // mask = (n > 64) ? mask : 0 and(m0, ptr [blk + rax * 8 + 0]); and(mask, ptr [blk + rax * 8 + 8]); popcnt(m0, m0); popcnt(rax, mask); 夏のプログラムシンポジウム2012 32 / 33
  • 33.
    select1のベンチマーク  メモリを大量に使うところでは速い marisa-trieのbit-vectorよりも速い  少ないところではオーバーヘッドがややある まだ改良の余地あり・あるいはnによって戦略を変える rankの処理時間(clk) fast 200 150 100 50 0 128KiB 0.5MiB 2MiB 8MiB 32MiB 0.1GiB 0.5GiB 2GiB 4GiB SBV1(org) SBV2 夏のプログラムシンポジウム2012 33 / 33