条件分岐とcmovとmaxps
TRANSCRIPT
![Page 1: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/1.jpg)
条件分岐とcmovとmaxps 条件分岐とcmovとmaxps
Cybozu Labs
2011/8/6 光成滋生
x86/x64最適化勉強会#1
![Page 2: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/2.jpg)
内容 内容
自己紹介
最適化の心得
プロファイル
ベンチマーク環境
条件分岐は重たい
条件分岐を減らす古典的なテクニック
cmovを使う
条件分岐は重たい?
SIMD命令を使う
maxps, pmaxsd /24 2
![Page 3: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/3.jpg)
自己紹介 自己紹介
光成滋生(サイボウズ・ラボ)
姑息な最適化が大好き
以前はコーデック系の趣味・仕事をやっていた
最近は仕事のために機械学習を勉強中
PRMLあんちょこ公開中(https://github.com/herumi/prml/)
blog : http://homepage1.nifty.com/herumi/
mail : [email protected]
twitter : @herumi
/24 3
![Page 4: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/4.jpg)
最適化の心得 最適化の心得
プログラム最適化の第一法則
最適化するな
プログラム最適化の第二法則
まだするな
そんなことはわかってる
人たちが集まってるよね…
/24 4
![Page 5: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/5.jpg)
プロファイル プロファイル
プロファイラ
関数組み込みタイプ
プログラム自体に組み込まれる
それ自体がプログラムに影響を与える
回数は正確
gprofやDevPartner, Vtune
タイミング割り込みによる集計タイプ
割り込みでその瞬間のeipを取得し集計
プログラムに影響を(殆ど)与えない
回数は不正確,時間は概ね正確
CodeAnalyst, perf, VTune
推測するな,実測せよ(もちろん仮説と検証も大事)
/24 5
![Page 6: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/6.jpg)
rdtsc rdtsc
nsecレベルの時間取得が可能
時刻取得システムコールはそれほど分解能が高くない
HPET(High Precision Event Timer)が使えるなら多少はましだが
最近のCPUの周波数が可変だと多少困る
マルチコアだと多少困る
プロセッサを判別できるrdtscpというのもある
シリアライズされてない,それ自体が若干時間を食う
が,お手軽なのがよい(以下はXbyakでの例)
/24 6
Xbyak::util::Clock clk; clk.begin(); 計測したい部分 clk.end(); printf("%d¥n", clk.getClock());
![Page 7: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/7.jpg)
ベンチマーク環境 ベンチマーク環境
今回のコード
https://github.com/herumi/opti/
要Xbyak http://homepage1.nifty.com/herumi/soft/xbyak.html
テスト環境
Xeon X5650 2.67GHz + Linux 2.6.32 + gcc 4.6.0
Core i7-2600 CPU 3.40GHz + Linux 2.6.35 + gcc 4.4.5
ただしWindows 7上のVMware上
Core i7-2600 Cpu 3.40GHz + Windows7(64bit) + VC2010
/24 7
![Page 8: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/8.jpg)
古典的なテクニック 古典的なテクニック
範囲チェック
分岐が2回
次のようにすると分岐は1回
後半の文字列で再度登場 /24 8
unsigned int a, b, x; if (a <= x && x <= b) { ... }
cmp x, a jb #elese cmp x, b ja #else ...
if ((x – a) <= (b – a)) { ... }
![Page 9: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/9.jpg)
条件分岐は重たい 条件分岐は重たい
不要な分岐を除去せよ
Intel64 and IA-32 Architectures Optimization Reference Manual コーディング規則1
CPUのパイプラインを乱す
大抵10段以上ある
スループットがあがらない
/24 9
![Page 10: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/10.jpg)
例題1. countMax 例題1. countMax
次の関数を最適化せよ
分岐命令をcmovやsetgに置き換えよ
(注意)x[i], y[i] >= 0を仮定する
/24 10
size_t countMax_C(const int *x, const int *y, size_t n) { size_t count = 0; for (size_t i = 0; i < n; i++) { if (x[i] > y[i]) count++; } return count; }
![Page 11: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/11.jpg)
jmp命令を使う jmp命令を使う
jmp-cmov.cpp抜粋
1ループあたり10~12clk
重たい
/24 11
L(".lp"); mov(t, ptr [x + n * 4]); // x[i]を読んで cmp(t, ptr [y + n * 4]); // y[i]と比較して jle(".skip"); // x[i] <= y[i]ならskip add(a, 1); // count++ L(".skip"); add(n, 1); jne(".lp");
![Page 12: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/12.jpg)
setg命令を使う setg命令を使う
setXXは条件が満たされたときに8bitレジスタを1に設定する(外れたら0)命令
8bitなのが結構使いにくい
jmp-cmov.cpp抜粋
1ループあたり2~2.5clk! /24 12
L(".lp"); mov(edx, ptr [x + n * 4]); // x[i]を読んで cmp(edx, ptr [y + n * 4]); // y[i]と比較して setg(dl); // dl = x[i] > y[i] ? 1 : 0 movzx(edx, dl); // 上位24bitを0クリア add(eax, edx); // countに加算 add(n, 1); jne(".lp");
![Page 13: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/13.jpg)
adc命令を使う adc命令を使う
adcはcarryつきadd命令
cmpしたときにcarry = y[i] < x[i] ? 1 : 0
eax = eax + 0 + carry = eax + (x[i] > y[i] ? 1 : 0);
(注意)符号無しの計算になるのでx[i], y[i] >= 0を仮定
/24 13
L(".lp"); mov(edx, ptr [y + n * 4]); // y[i]を読んで cmp(edx, ptr [x + n * 4]); // x[i]と比較して adc(eax, 0); // ??? add(n, 1); jne(".lp");
![Page 14: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/14.jpg)
条件分岐は重たい? 条件分岐は重たい?
計ってみると1ループあたり1clk程度?
実は条件分岐の予測がはずれたとき重たい
最近のCPUの条件分岐予測機構は優秀なので結構当たる
あたった場合のコストはかなり低い
/24 14
L("@@"); sub(ecx, 1); jnz("@b");
![Page 15: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/15.jpg)
条件分岐は重たい? 条件分岐は重たい?
与えられた配列の最大値の取得(cmov vs. jge)
/24 15
int getMax_C(const int *x, size_t n) { int max = x[0]; for (size_t i = 1; i < n; i++) { if (x[i] > max) max = x[i]; } return max; }
// a = max cmp(a, ptr [x + n * 4]); // if (a > x[i]) a = x[i] cmovl(a, ptr [x + n * 4]);
// a = max cmp(a, ptr [x + n * 4]); jge(".skip"); mov(a, ptr [x + n * 4]); L(".skip");
![Page 16: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/16.jpg)
ベンチマーク ベンチマーク
ランダムな値を入れた配列に対して cmovが2.7clkなのに対してjmpは1.8clk!
jmpの方が速い
ランダムな配列の最大値はどこか途中で出てくると それ以降は必ずそれより小さい
分岐予測がヒットする
最適化マニュアルを見直す(コーディング規則2)
setcc, cmovを使って分岐を排除せよ. ただし,予測可能な分岐に対しては排除してはいけない
さて,配列の最大値で最後まで予測できないのは不規則に単調増加しているとき.そんなケースは多くなさそう
/24 16
![Page 17: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/17.jpg)
大抵はjmpが速そうだ(@i7)
ちなみに最初のcountMaxのベンチマーク
x[i] > y[i]となる割合
一番予測できないときjmpの数値が非常に悪い
もう少しベンチマーク もう少しベンチマーク
ランダム 最初が一番大きい 単調増加 時々増加
jmp 1.859 1.823 1.836 3.637
cmov 2.741 2.718 2.747 2.751
/24 17
rate 0.00 0.25 0.50 0.75 1.00
jmp 1.863 7.827 12.747 9.276 1.854
setg 2.054 2.043 2.052 2.026 2.054
adc 1.842 1.838 1.849 1.828 1.822
![Page 18: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/18.jpg)
分岐点はどこか 分岐点はどこか
概ね経験則で分岐予測がヒットしたとき<1clk 外れたとき20clk
先程もrate=50%のときが12clk程度
cmovなどの条件分岐排除命令は数clk
ざっくり,分岐予測が1/10しか外れないなら互角かも
データがどういう性質を持っているかをよく見極めることが大切
/24 18
![Page 19: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/19.jpg)
SIMDを使ってみる SIMDを使ってみる
maxps = max(x[i], y[i]) for i = 0..3 を求める
SIMD凄い! /24 19
// xm0 = [x[3]:x[2]:x[1]:x[0] L("@@"); maxps(xm0, ptr [x + n * 4]); // update xm0 add(n, 4); jnz("@b"); // ループが終わったらxm0のうちの一番大きいものを求める
i7 ランダム 最初が一番大きい 単調増加 時々増加
jmp 1.859 1.823 1.836 3.637
cmov 2.741 2.718 2.747 2.751
maxps 0.686 0.689 0.677 0.687
![Page 20: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/20.jpg)
実はちょっとした落とし穴 実はちょっとした落とし穴
Xeon, Core2Duoなどのプロセッサでは劇遅
Xeonだと30clk以上!
PentiumDだと速かったりする
maxpsは浮動小数点数(float)に対する命令
普通の整数値はfloatとしてみると非正規化数になっちゃうことも多い
ペナルティを食らう
float x[N]に対する先程の命令ではXeonだと0.648clk
PentiumDの頃は型にうるさく無かった?
整数に対するmax命令はpmaxsd
SSE4.1にしか無いがmaxpsよりは速い /24 20
![Page 21: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/21.jpg)
ベンチマーク ベンチマーク
i7 ランダム 最初が一番大きい 単調増加 時々増加
jmp 1.859 1.823 1.836 3.637
cmov 2.741 2.718 2.747 2.751
maxps 0.686 0.689 0.677 0.687
pmaxsd 0.276 0.279 0.287 0.273
/24 21
Xeon ランダム 最初が一番大きい 単調増加 時々増加
jmp 3.414 1.745 1.746 3.481
cmov 5.008 2.613 2.612 2.653
maxps 30.741 26.581 26.587 26.547
pmaxsd 0.437 0.437 0.437 0.437
http://agner.org/optimize/blog/read.php?i=142
i7は非正規数の扱いが高速になった
![Page 22: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/22.jpg)
maxssよりjmpがよいレアケース maxssよりjmpがよいレアケース
floatに対する高速なexp, logの実装fmath.hpp
https://github.com/herumi/fmath/
gccの数倍から一桁以上速い
exp(x)はxがある値A以上で∞, -A以下で0とみなす
その範囲でクリッピングしたい
/24 22
Xeon exp expx4 log logx4
gcc 4.6 565.9 2265.7 54.1 222.7
fmath 15.2 35.4 12.9 32.3
minss(x, A); maxss(x, -A); ...
![Page 23: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/23.jpg)
maxssよりjmpがよいレアケース maxssよりjmpがよいレアケース
maxss/minssのレイテンシーは3clkぐらい
依存関係が高いので隠蔽しにくい
通常expが0になったり∞になったりする値は扱わない
範囲外はレアケースとして追い出す
これで15.9clk → 14.2clkと少し速くなった
/24 23
minps(x, A); maxps(x, -A); ...
movd(edx, x); and(edx, 0x7fffffff); ... // メインの作業 cmp(edx, A); // 整数として比較して jg(".overflow"); L(".overflow"); // レアケースの場合は minss(x, A); // やり直し maxss(x, -A); jmp(".retry");
![Page 24: 条件分岐とcmovとmaxps](https://reader033.vdocuments.net/reader033/viewer/2022052901/55660e25d8b42aa6628b535f/html5/thumbnails/24.jpg)
まとめ まとめ
条件分岐を排除せよ
ただし分岐予測ができないときのみ
意外とCPUの分岐予測に任せた方がよいこともある
cmov, setgは起こらなかったときのコストを薄く払う
可能ならSIMD命令を使う
トリッキーな非SIMDよりも簡単に速くできる(かも)
整数演算と浮動小数演算の命令に注意する
同じ内容でも異なる命令が割り当てられている
andpsとpand, movdqaとmovapsなど
SIMDよりもjmpがよいときもあることはある
いろんなパターンを考えよう /24 24