Recommended
PPT
PDF
PPTX
PDF
組み込み関数(intrinsic)によるSIMD入門
PDF
PDF
PDF
WASM(WebAssembly)入門 ペアリング演算やってみた
PDF
PDF
PDF
PPTX
BoostAsioで可読性を求めるのは間違っているだろうか
PDF
PPTX
PPTX
PDF
ARM CPUにおけるSIMDを用いた高速計算入門
PPT
PDF
PPTX
PDF
constexpr関数はコンパイル時処理。これはいい。実行時が霞んで見える。cpuの嬌声が聞こえてきそうだ
PDF
Intro to SVE 富岳のA64FXを触ってみた
PDF
20分くらいでわかった気分になれるC++20コルーチン
PDF
PDF
PPTX
PDF
PDF
PDF
PDF
PDF
PDF
More Related Content
PPT
PDF
PPTX
PDF
組み込み関数(intrinsic)によるSIMD入門
PDF
PDF
PDF
WASM(WebAssembly)入門 ペアリング演算やってみた
PDF
What's hot
PDF
PDF
PPTX
BoostAsioで可読性を求めるのは間違っているだろうか
PDF
PPTX
PPTX
PDF
ARM CPUにおけるSIMDを用いた高速計算入門
PPT
PDF
PPTX
PDF
constexpr関数はコンパイル時処理。これはいい。実行時が霞んで見える。cpuの嬌声が聞こえてきそうだ
PDF
Intro to SVE 富岳のA64FXを触ってみた
PDF
20分くらいでわかった気分になれるC++20コルーチン
PDF
PDF
PPTX
PDF
PDF
PDF
PDF
Similar to フラグを愛でる
PDF
PDF
PDF
PDF
PDF
PPTX
PDF
Intel GoldmontとMPXとゆるふわなごや
PDF
PDF
LLVMで遊ぶ(整数圧縮とか、x86向けの自動ベクトル化とか)
PDF
PDF
“Symbolic bounds analysis of pointers, array indices, and accessed memory reg...
PDF
x64 のスカラー,SIMD 演算性能を測ってみた @ C++ MIX #10
PDF
PDF
x64 のスカラー,SIMD 演算性能を測ってみた v0.1 @ C++ MIX #10
PDF
PDF
PPTX
PDF
PDF
PDF
More from MITSUNARI Shigeo
PDF
PDF
PDF
PDF
PDF
PDF
PDF
PDF
Intel AVX-512/富岳SVE用SIMDコード生成ライブラリsimdgen
PDF
PDF
PDF
PDF
PDF
PDF
深層学習フレームワークにおけるIntel CPU/富岳向け最適化法
PDF
PDF
PDF
Lifted-ElGamal暗号を用いた任意関数演算の二者間秘密計算プロトコルのmaliciousモデルにおける効率化
PDF
PDF
PDF
Recently uploaded
PDF
100年後の知財業界-生成AIスライドアドリブプレゼン イーパテントYouTube配信
PDF
さくらインターネットの今 法林リージョン:さくらのAIとか GPUとかイベントとか 〜2026年もバク進します!〜
PDF
第21回 Gen AI 勉強会「NotebookLMで60ページ超の スライドを作成してみた」
PDF
Reiwa 7 IT Strategist Afternoon I Question-1 3C Analysis
PDF
2025→2026宙畑ゆく年くる年レポート_100社を超える企業アンケート総まとめ!!_企業まとめ_1229_3版
PDF
PDF
Reiwa 7 IT Strategist Afternoon I Question-1 Ansoff's Growth Vector
PDF
Starlink Direct-to-Cell (D2C) 技術の概要と将来の展望
PPTX
フラグを愛でる 1. 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