Transcript
Page 1: フラグを愛でる

フラグを愛でる

2013/3/30 光成滋生(@herumi)

x86/x64最適化勉強会5(#x86opti)

Page 2: フラグを愛でる

目次

さまざまなコード片でcarryの扱いを味わってみる

フラグの復習

絶対値の復習

ビットの長さ

ビットカウント

cybozu/WaveletMatrixの紹介

多倍長整数加算

Haswellで追加された命令達

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

2013/3/30 #x86opti 5 /24 2

Page 3: フラグを愛でる

フラグの復習

演算ごとに変化する1bitの情報群

よく使うもの

ZF : Zero flag : 演算結果が0ならtrue

CF : Carry flag : 演算に桁上がり、桁借りがあればtrue

SF : Sign flag : 演算結果が負ならtrue

条件つきmov命令

フラグの条件が成立すれば代入

cmovz eax, ecx ; ZF = 1ならeax ← ecx

2013/3/30 #x86opti 5 /24 3

mov eax, 5 neg eax ; -5になるのでSF = 1, ZF = 0

mov eax, 0x80000001 mov ecx, 0x80000002 add eax, ecx ; 32bitを超えるのでCF = 1

Page 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を分岐なしで実行できる

古典的なトリック

2013/3/30 #x86opti 5 /24 4

int abs(int x) { uint32_t M = (x >> 31); // M = x >= 0 ? 0 : (-1); return x ^ M – M; }

Page 5: フラグを愛でる

絶対値(2/3)

gcc 4.7での実装

clang 3.3での実装

わかりやすい

sandyだと少し速い(古いCPUだとgccのがよいことも)

2013/3/30 #x86opti 5 /24 5

// 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

mov eax, ecx ; eax = x mov edx, ecx ; edx = x neg eax ; eax = -x cmovl eax, edx ; eax = (x > 0) ? x : -x

Page 6: フラグを愛でる

絶対値(3/3)

VCでの実装

cmovより速い(ことが多い)

cmov命令はIntel CPUではものすごく速いというわけではない

core2duo, sandy bridgeと段々よくなったが

レジスタ固定だが まあいまどきmovはほぼコスト0だし

64bitだとcqo(0x48 0x99)

VCのcod出力はcdqと表示されるので注意

2013/3/30 #x86opti 5 /24 6

mov eax, ecx cdq ; M = edx = (eax < 0) ? -1 : 0 xor eax, edx ; eax = x^ M sub eax, edx ; eax = x^ M - M

Page 7: フラグを愛でる

ビットの長さ(1/4)

xを確保するのに必要なビットの長さ

x == 0のとき1とする

__builtin_clzを使う

これはcount leading zeroなので32から結果を引く

この関数はx == 0のときは未定義なので別に処理する

2013/3/30 #x86opti 5 /24 7

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; }

if (x == 0) return 1; return 32 - __builtin_clz(x);

Page 8: フラグを愛でる

ビットの長さ(2/4)

gcc 4.7とclang 3.3

clangの方がちょっと賢い感じだがなんか微妙

bsr(x) == 32 - __builin_clz(x)なので回りくどい

少し変えてみる

2013/3/30 #x86opti 5 /24 8

// 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

if (x == 0) return 1; // return 32 - __builtin_clz(x); return (__builtin_clz(x) ^ 0x1f) + 1;

Page 9: フラグを愛でる

ビットの長さ(3/4)

なぜかgccだけよくなった

xorとaddがキャンセルした

VCでは_BitScanReverse(&ret, x)を使う

これはx == 0のときfalseを返す

2013/3/30 #x86opti 5 /24 9

unsigned long ret; if (_BitScanReverse(&ret, x)) return ret + 1; return 1;

// 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

Page 10: フラグを愛でる

ビットの長さ(4/4)

VCはbsrは入力が0ならZF=1なことを知っている

いや, でもZF = 1のときはecx = 0なんだし こうすればすっきりする

ただしx == 0が殆どありえないなら上の方が速いかも

2013/3/30 #x86opti 5 /24 10

bsr eax, ecx cmovz eax, ecx inc eax ret

bsr eax, ecx je .zero inc eax ret .zero: mov eax, 1 ret

Page 11: フラグを愛でる

減算のあとのmod p(1/2)

暗号ではX={0, 1, ..., p-1}の中の四則演算をよく使う

x, y ∈ Xに対して(x + y) % pとか(x – y) % pとか

大小比較って結局は引いてみないと分からない

分岐はランダムなので10clk以内なら条件jmpは避けたい

2013/3/30 #x86opti 5 /24 11

// 引き算の疑似コード // 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; }

Page 12: フラグを愛でる

減算のあとのmod p(2/2)

sub + sbb後のCFを利用してaddすべき値をcmov

256bit減算なので64bitレジスタ x 4を使う

実際にはcmovなどが4個並んでる

ルール : 分岐予測不可→cmov→可能なら単純命令に

0に設定してcmovよりマスクして&が少し速い(CPUによる)

2013/3/30 #x86opti 5 /24 12

// 疑似コード 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]

sbb m, m // m = (x < y) ? -1 : 0 [t3:t2:t1:t0] = [p3:p2:p1:p0] [t3:t2:t1:t0] &= m

Page 13: フラグを愛でる

128bit popcnt(1/3)

Wavelet行列の中で使う簡潔ベクトル構造の中

結局, 今のところ使わなかったけど面白かったので紹介

idxから128bit分のマスクを作って[b1:b0]と&をとってpopcnt

idx&127>=64なら[m1:m0]=[*:-1]。<64なら[m1:m0]=[0:*]

2013/3/30 #x86opti 5 /24 13

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; }

| 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 |

Page 14: フラグを愛でる

128bit popcnt(2/3)

gcc 4.7

ジャンプ命令を使う…論外

VC2012

idx & 64 == 0の判定を2回する…おしい

clang 3.3

idx & 64 == 0の判定は1回だがなぜかシフト

しかも作ったフラグをつぶす(clangあるある)

2013/3/30 #x86opti 5 /24 14

// edx = idx and edx, 64 shr edx, 6 xor ecx, ecx test edx, edx cmovneq rcx, rax mov rdx, -1 cmoveq rdx, rax

ZFを保存するため 順序入れ換える xor ecx, ecx and edx, 6 cmovneq rcx,rax mov rdx, -1 cmoveq rdx, rax

最適解? ecxとedxを入れ換えたら xor不要 or rcx, -1 ;3byte減る and edx, 6 cmovneq rdx, rax cmoveq rcx, rax

Page 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回)

wat_arrayは岡野原さん, wavelet-matrix-cppはmanabeさん作

wmに比べてもcyはrankが約3.2倍, lookupが2.5倍

2013/3/30 #x86opti 5 /24 15

実装 時間[sec] lookup[clk] rank[clk]

オリジナル(wat_array) 160 1887 1887

wavelet-matrix-cpp(wm) 72 883 598

cybozu/WaveletMatrix(cy) 30 343 183

Page 16: フラグを愛でる

多倍長整数の加算(1/5)

前半発表のlliの出力(一部)

なんかえらいことに

実はとても遅いというわけではなかったり(たまたま)

2013/3/30 #x86opti 5 /24 16

.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

Page 17: フラグを愛でる

多倍長整数の加算(2/5)

加算でやらしいところ

carryつきaddを実行したいのでcarryを変更してはいけない

でもループ変数はいじらないといけない

コンパイラに任せると先程のフラグを保存するコードになる

抜け道

add, sub, adc, sbbはCFとZFを変更するが inc, decはCFを変更しない

ループカウンタcを-nから0方向にインクリメント

x, y, zのアドレスはあらかじめn * 8を足してずらしておく

2013/3/30 #x86opti 5 /24 17

.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");

Page 18: フラグを愛でる

多倍長整数の加算(3/5)

Xeon X5650(Westmere)では

ループあたり13clkもかかる

なんと先程のLLVMの(酷い)コードよりも遅い!

フラグに関するパーシャルレジスタストール

Intelの最適化マニュアル 「INCとDECはADDやSUBに置き換えるべきだ」

置き換えたら動かないんですけど

2013/3/30 #x86opti 5 /24 18

.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");

Page 19: フラグを愛でる

多倍長整数の加算(4/5)

jrcxz/jecxz命令

rcx, ecxがゼロなら分岐する命令

みなさん覚えてますか? 私は忘れてました

Pentiumで遅かったのでloop命令とともに封印された(私の中で)

jnrcxzは無いのでループで使うとねじれるのが難

core2duo以降はそこそこ速い

16回ループ(1024bit加算)が208clk→62clk

2013/3/30 #x86opti 5 /24 19

.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");

Page 20: フラグを愛でる

多倍長整数の加算(5/5)

Sandy Bridgeでは改良された

元のコード(adc + dec)の方が速い

先頭だけ外に出す微調整でもっと速くなった(なぜ)

https://github.com/herumi/opti/blob/master/uint_add.cpp

多倍長の乗算

同様にmulがフラグを変更するのが邪魔

レジスタの使い回しで非常に苦労する

速度低下にもつながる

2013/3/30 #x86opti 5 /24 20

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

read+modify+writeが2clk/64bit

Page 21: フラグを愛でる

Haswellで追加された命令(1/3)

無視されるフラグたち

未定義だった部分が確定した命令

2013/3/30 #x86opti 5 /24 21

命令 動作

adcx 符号なし加算(CFのみ変更)

adox 符号なし加算(OFのみ変更)

mulx 符号なし乗算(フラグ変更なし)

sarx 算術右シフト(フラグ変更なし)

shlx 論理左シフト(フラグ変更なし)

shrx 論理右シフト(フラグ変更なし)

rorx 論理右回転(フラグ変更なし)

lzcnt bsrの拡張

tzcnt bsfの拡張

Page 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 /24 22

Page 23: フラグを愛でる

Haswellで追加された命令(3/3)

pdep(x, mask)

maskのビットがたっているところにxのビットを埋め込む

pext(x, mask)

maskのビットが立っているところのxのビットを取り出す

2013/3/30 #x86opti 5 /24 23

x5 x4 x3 x2 x1 x0

1 1 0 0 1 0

x

mask

x2 x1 0 0 x0 0 result

x5 x4 x3 x2 x1 x0 x

1 1 0 0 1 0 mask

. . . x5 x4 x1 result

Page 24: フラグを愛でる

おまけ

今どきのコンパイラはかしこい

ハッシュ関数(fnv-1a)のループ内演算 https://github.com/herumi/misc/blob/master/fnv-1a.cpp

「v += シフト&加算」の部分はv *= p(41bitの素数)の形

pのハミング重み(2進数展開の1の数)が小さいものを探して選ぶ

gcc 4.7は素直にleaやaddやshlを組み合わせたコード生成

clang, VCはmulに置き換えた!

こっちのほうが2.4倍速い@i7

ハミング重みにこだわらない 関数探索もありか?

2013/3/30 #x86opti 5 /24 24

for (size_t i = 0; i < n; i++) { v ^= x[i]; v += (v<<1)+(v<<4)+(v<<5)+(v<<7)+(v<<8)+(v<<40); }

mov r10, 1099511628211 ; p .lp: movzx r8d, byte [r9+rcx] inc r9 xor rax, r8 imul rax, r10


Top Related