More Related Content More from MITSUNARI Shigeo More from MITSUNARI Shigeo (20) フラグを愛でる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