フラグを愛でる
TRANSCRIPT
![Page 1: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/1.jpg)
フラグを愛でる
2013/3/30 光成滋生(@herumi)
x86/x64最適化勉強会5(#x86opti)
![Page 2: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/2.jpg)
目次
さまざまなコード片でcarryの扱いを味わってみる
フラグの復習
絶対値の復習
ビットの長さ
ビットカウント
cybozu/WaveletMatrixの紹介
多倍長整数加算
Haswellで追加された命令達
題材があちこち飛びます m(__)m
2013/3/30 #x86opti 5 /24 2
![Page 3: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/3.jpg)
フラグの復習
演算ごとに変化する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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/4.jpg)
絶対値(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/5.jpg)
絶対値(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/6.jpg)
絶対値(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/7.jpg)
ビットの長さ(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/8.jpg)
ビットの長さ(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/9.jpg)
ビットの長さ(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/10.jpg)
ビットの長さ(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/11.jpg)
減算のあとの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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/12.jpg)
減算のあとの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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/13.jpg)
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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/14.jpg)
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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/15.jpg)
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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/16.jpg)
多倍長整数の加算(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/17.jpg)
多倍長整数の加算(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/18.jpg)
多倍長整数の加算(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/19.jpg)
多倍長整数の加算(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/20.jpg)
多倍長整数の加算(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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/21.jpg)
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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/22.jpg)
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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/23.jpg)
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: フラグを愛でる](https://reader034.vdocuments.pub/reader034/viewer/2022042816/5588fbced8b42a4a1a8b466c/html5/thumbnails/24.jpg)
おまけ
今どきのコンパイラはかしこい
ハッシュ関数(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