Upload
mitsunari-shigeo
View
2.802
Download
0
Embed Size (px)
Citation preview
高機能アセンブラによるx86/x64 CPU向け高速化テクニック
Cybozu Labs
2012/8/24 光成滋生
目次
x86/x64アセンブラの復習
C++用のx86/x64アセンブラXbyakの紹介
サンプル
文字列検索
ビットを数える
自己紹介
x86/x64最適化勉強会主催
https://github.com/herumi/opti/
http://www.slideshare.net/herumi/
/ 332夏のプログラムシンポジウム2012
x86/x64アセンブラの復習(1/2)
汎用レジスタは15個(64bit時)
rax, rbx, ..., r8, ..., r15 ; 64bitレジスタ
rsp : スタックレジスタ
基本的に2オペランド
<op> <dst> <src> // dst ← op(dst, src);
dstは破壊される
アドレッシングは比較的高機能
ptr [<reg1> + <reg2> * (0|1|2|4|8) + <即値>]
/ 333
例)mov rax, rcx // rax = rcx;add ecx, 4 // ecx += 4;sub r8,[rax+ebx*4+12]//r8 -= *(int64_t)(rax+ebx*4+12);
夏のプログラムシンポジウム2012
x86/x64アセンブラの復習(2/2)
SIMDレジスタは16個(64bit時)
xmm0, ..., xmm15 ; 128bitレジスタ
ymm0, ..., ymm15 ; 256bitレジスタ
データ型
char x 16, int x 4, float x 8, double x 4, etc.
演算の種類
四則演算,ビット演算,特殊演算,etc.
3オペランドタイプもある
/ 334
例)movaps xmm0,[eax] //xmm0にfloat変数4個が代入されるvaddpd ymm0,ymm3,ymm2 //ymm0 = ymm3 + ymm2(double x 4)pand xmm2, xmm4 //xmm2 &= xmm4
夏のプログラムシンポジウム2012
Xbyakの特長
C++ヘッダのみで記述
外部ライブラリのビルドやリンクが不要
対応コンパイラ : Visual Studio/gcc/clang
対応OS : Windows/Linux/Mac
実行時コード生成
Intel MASM形式に似せたDSLを提供
できるだけ自然に記述できるようにした
アドレッシングは設計時に一番悩んだ部分
かつ一番気に入ってる部分
/ 335
mov(eax, 5); // mov eax, 5add(rcx, byte[eax+ecx*4–5]);//add rcx,byte[eax+ecx*4-5]jmp("lp"); // jmp lp
夏のプログラムシンポジウム2012
簡単なサンプル(1/3)
整数nが与えられたときにnを足す関数を生成
ヘッダをincludeしてCodeGeneratorを継承
/ 336
#include <xbyak.h>
struct Code : Xbyak::CodeGenerator {explicit Code(int n) {mov(eax, ptr [esp + 4]); // 32bit OSadd(eax, n);// lea(rax, ptr [rcx + n]); // 64bit Windows// lea(eax, ptr [edi + n]); // 64bit Linux/Macret();
}};
夏のプログラムシンポジウム2012
簡単なサンプル(2/3)
インスタンスを生成して実行
/ 337
int main(int argc, char *argv[]) {const int n = argc == 1 ? 5 : atoi(argv[1]);Code c(n);auto f = (int (*)(int))c.getCode();for (int i = 0; i < 3; i++) {
printf("%d + %d = %d\n", i, n, f(i));}
}
% ./a.out 90 + 9 = 91 + 9 = 102 + 9 = 11
夏のプログラムシンポジウム2012
簡単なサンプル(3/3)
インスタンスを生成して実行
引数が9のときの関数fの中身をデバッガで確認
32bit Windowsで見てみた
引数が3のときの関数fの中身をデバッガで確認
64bit Linuxで見てみた
/ 338
mov eax,dword ptr [esp+4]add eax,9 // ここが即値ret
0x0000000000607000 in ?? ()1: x/i $pc
0x607000: lea eax,[edi+0x3]// ここが即値
夏のプログラムシンポジウム2012
応用例
ビューティフルコード8章
画像処理のためのその場コード生成
画像処理では関数内では固定のパラメータが多い
templateなどを組み合わせるとバイナリサイズが肥大化
/ 339夏のプログラムシンポジウム2012
画像処理のためのその場コード生成
ビットマップD, Sを合成変換する関数
変換種類を示すopがループの最内部分にあるため通常の実装では遅すぎる
1985年のWindowsではそのコードを生成するミニコンパイラを含んでいたらしい
/ 3310
for (int y = 0; y < cy; y++) {for (int x = 0; x < cx; x++) {
switch (op) {case 0x00: D[y][x] = 0; break;...case 0x60: D[y][x] = (D[y][x] ^ S[y][x]) & P[y][x];...} } }
夏のプログラムシンポジウム2012
Xbyakによるその場コード生成
メリット
C++の文法でasmの制御構造を記述できる
極めて直感的に扱える
アセンブラ独自の疑似命令を覚える必要がない
Cの構造体との連携がシームレス
構造体メンバのオフセット取得は外部ツールでは困難
Xbyakなら<stddef.h>のoffsetof(type, member)をそのまま利用可能
/ 3311
Code(const Rect& rect, int op){// generate prolog
L(".lp_y");mov(rcx, rect.width);
L(".lp_x");switch (op) {..case 0x60:
mov(eax,ptr[ptrD+rbx]);xor(eax,ptr[ptrS+rbx]);and(eax,ptr[ptrP+rbx]);break;
}mov(ptr[ptrD + rbx], eax);add(rbx, 4);sub(rcx, 1);jnz(".lp_x");...
夏のプログラムシンポジウム2012
FFTのバタフライ演算(1/2)
ビット反転とデータ移動
入力は次数nとデータa
ipはビット反転用work
nは可変だが,128とか256とかが多いケース
通常は事前に専用コードを用意して分岐する
ツールでCのコードを生成するなど
/ 3312
void swap(double*a,int k1,int j1){__m128d x = _mm_load_pd(a + j1);__m128d y = _mm_load_pd(a + k1);__mm_store_pd(a + j1, y);__mm_store_pd(a + k1, x);}void bitrv(int n,int *ip,double *a){int j, j1, k, k1, l, m, m2;l = n; m = 1;...m2 = 2 * m;if ((m << 3) == l) {
for (k = 0; k < m; k++) {for (j = 0; j < k; j++) {
j1 = 2 * j + ip[k];k1 = 2 * k + ip[j];swap(a, j1, k1);
夏のプログラムシンポジウム2012
FFTのバタフライ演算(2/2)
修正箇所はわずか
swap()をコード生成するように変更
関数のプロローグとエピローグを作る
生成コード例
定数とループが全て展開される
/ 3313
struct Code : Xbyak::CodeGenerator {void swap(const Reg32e& a, int k1, int j1){movapd(xm0, ptr [a + j1 * 8]);movapd(xm1, ptr [a + k1 * 8]);movapd(ptr [a + j1 * 8], xm1);movapd(ptr [a + k1 * 8], xm0);
}void gen_bitrv2(int n, int *ip){
const Reg64& a = rdi;int j, j1, k, k1, l, m, m2;...
j1 = 2 * j + ip[k];k1 = 2 * k + ip[j];swap(a, j1, k1);...
movapd xm0,ptr [eax+10h]movapd xm1,ptr [eax+100h]movapd ptr [eax+10h],xm1movapd ptr [eax+100h],xm0movapd xm0,ptr [eax+50h]movapd xm1,ptr [eax+140h]...
夏のプログラムシンポジウム2012
SSE4.1の文字列探索命令の紹介
pcmpestri, pcmpistriなど
strlenやstrstrなどを高速に実装するための命令群
複雑なパラメータを持つCISCの権化の様な命令
文字列処理の単位char or short(2byte:UTF16)の選択
符号付き・符号無しの選択
文字列の比較方法の選択
完全マッチ,文字の範囲指定,部分文字列など
入力文字列の設定
0ターミネート文字列 or [begin, end)による文字列
CF, ZF, SF, OFの各フラグに結果の様々な情報
詳細
http://www.slideshare.net/herumi/x86opti3
/ 3314夏のプログラムシンポジウム2012
strstrを作ってみる
16byteずつ文字列を探索する
入力パラメータ
a : 入力テキスト(text)ポインタが格納されたレジスタ
xm0 : 検索文字(の先頭最大16byte)
12 : 文字列マッチをuint8_tの部分文字マッチさせる即値
出力フラグ
CF : 文字列を見つける前にtextが終われば1
ZF : aから16byte内に文字列がなければ0
/ 3315
movdqu(xm0, ptr [key]); // xm0 = *keyL(".lp");pcmpistri(xm0, ptr [a], 12);lea(a, ptr [a + 16]);ja(".lp");jnc(".notFound");
夏のプログラムシンポジウム2012
strstrのコア部分(続き)
keyの先頭文字が一致する部分を検出した
先程のループを抜けたときはcにマッチしたオフセットが入っているのでそれだけ進める(add a, c)
その場所からkeyの最後まで一致しているかを確認
OF == 0なら一致しなかった
SF == 1なら見つかった
/ 3316
add(a, c);mov(save_a, a); // save amov(save_key, key); // save key
L(".tailCmp");movdqu(xm1, ptr [save_key]);pcmpistri(xm1, ptr [save_a], 12);jno(".next"); // if (OF == 0) goto .nextjs(".found"); // if (SF == 1) goto .foundadd(save_a, 16);add(save_key, 16);jmp(".tailCmp");
夏のプログラムシンポジウム2012
ベンチマーク
対象CPU, コンパイラ
Xeon X5650 + gcc 4.6.3
対象コード
gccのstrstr(これもSSE4.1を利用している)
boost 1.51のalgorithm::boyer_moore(BM法)
Quick Search(改良版BMアルゴリズム)
my_strstr
検索方法
テキスト:130MBのUTF8な日本語を多く含むもの
指定されたkey文字列がいくつあるかを探す
byte単位あたりにかかったCPU clock数を表示する
/ 3317夏のプログラムシンポジウム2012
strstrのベンチマーク
Xeon X5650 + gcc 4.6.3
/ 3318
a ab 1234 これはstatic_ass
ert00...0 AB...Z
string::find 3.37 3.04 3.23 7.39 2.95 3.27 2.8
boost::bm 6.76 6.27 3.23 1.74 1.07 0.93 0.56
quick search 4.7 3.12 1.99 3.4 0.85 0.7 0.54
strstr(gcc) 1.64 1.13 1.12 1.15 1.11 1.18 0.46
my_strstr 0.6 0.3 0.29 0.8 0.28 0.3 0.27
0
1
2
3
4
5
6
7
8
cycl
e/B
yte
to
fin
d
string::find
boost::bm
quick search
strstr(gcc)
my_strstr
fast
夏のプログラムシンポジウム2012
考察
ABC....Zの様なBM法やQuick Searchアルゴリズムにとって有利な文字列すらstrstrより遅い
BM法やQuick Searchはテーブルを引いてオフセットを足すためパイプラインに悪影響がある
通常の文字列検索では出番がない?
/ 3319
// Quick Searchの検索部分const char *find(const char *begin, const char *end) const {
while (begin <= end - len_) {for (size_t i = 0; i < len_; i++) {
if (str_[i] != begin[i]) goto NEXT;}return begin;
NEXT:begin += tbl_[static_cast<unsigned char>(begin[len_])];
}return end;
}
夏のプログラムシンポジウム2012
strcasestrの実装
keyの大文字小文字を区別しないstrstr
コードの簡略さのためにkeyは事前に小文字化しておく
大文字小文字を区別しないマッチ方法
textを小文字にしてマッチさせる(先程のコードに委譲)
/ 3320
movdqu(xm0, ptr [key]); // xm0 = *keyL(".lp");if (caseInsensitive) {movdqu(xm1, ptr [a]);toLower(xm1, Am1, Zp1, amA, t0, t1);//小文字化するコード生成関数pcmpistri(xm0, xm1, 12);
} else {pcmpistri(xm0, ptr [a], 12);
}lea(a, ptr [a + 16]);ja(".lp"); // if (CF == 0 and ZF = 0) goto .lpjnc(".notFound");
夏のプログラムシンポジウム2012
toLower(1/2)
大文字を小文字にする
'A' <= c && c <= 'Z'なら c += 'a' – 'Z'
分岐無しで16byteずつまとめてやりたい
pcmpgtb x, y
x ← x > y ? 0xff : 0;をbyte単位で行う関数
if ('A' <= c && c <= 'Z') c += 'a' – 'Z';を書き換える
/ 3321
('A' <= c && c <= 'Z') ? ('a' – 'Z') : 0;= ((c > 'A'–1) ? 0xff : 0) & (('Z'+1 > c) ? 0xff : 0) & ('a'–'Z');= pcmpgtb(c, 'A'-1) & pcmpgtb('Z'+1, c) & 'a'-'Z';
夏のプログラムシンポジウム2012
toLower(2/2)
実際のコード片
/ 3322
/*toLower in xAm1 : 'A' – 1, Zp1 : 'Z' + 1, amA : 'a' - 'A't0, t1 : temporary register
*/void toLower(const Xmm& x, const Xmm& Am1, const Xmm& Zp1
, const Xmm& amA , const Xmm& t0, const Xmm& t1) {movdqa(t0, x);pcmpgtb(t0, Am1); // -1 if c > 'A' - 1movdqa(t1, Zp1);pcmpgtb(t1, x); // -1 if 'Z' + 1 > cpand(t0, t1); // -1 if [A-Z]pand(t0, amA); // 0x20 if c in [A-Z]paddb(x, t0); // [A-Z] -> [a-z]
}
夏のプログラムシンポジウム2012
CPU判別によるディスパッチ
コア部分再度(詳細)
実験によるとCPUの世代でコードの書き方で速度が違う(lea vs add)
実行時にCPU判別することで適切なコード生成を行う
10%程度速度向上があった
/ 3323
pcmpistri(xm0,ptr[a],12);if (isSandyBridge) {lea(a, ptr [a + 16]);ja(".lp");
} else {jbe(".headCmp");add(a, 16);jmp(".lp");
L(".headCmp");}jnc(".notFound");if (isSandyBridge) {
lea(a,ptr[a+c-16]);} else {
add(a, c); }
夏のプログラムシンポジウム2012
ビットを数える
簡潔データ構造
サイズnの{0, 1}からなるビットベクトルvに対してrank(x) = #{ v[i] = 1 | 0 <= i < x }select(m) = min { i | rank(i) = m }を基本関数として圧縮検索などさまざまなロジックに利用
(注)普通はrank()は0 <= i <= xの範囲で定義
rankはまさにビットカウント関数
ビューティフルコード10章「高速ビットカウントを求めて」の続き?
ここでは32bit/64bitに対するビットカウント命令popcntの存在は前提
/ 3324夏のプログラムシンポジウム2012
ナイーブな実装
岡野原さんの2006年CodeZine記事ベース
256bitごとの累積値をuint32_t a[]に保存
a[i] := rank(256 * i)
そこからの64bitごとの差分累積値をuint8_t b[]に保存
b[i] := rank(64 * i) – rank(64 * (i & ~3))
必要なメモリは(32 + 8 * 4) / 256 = 1/4(bitあたり)
rank(x) = a[i/256] + b[i/64]+ popcnt(v[i/64] & mask);
ランダムアクセスするのでキャッシュミスが頻出
/ 3325
0 1 1 0..
0..
1 0 1 1 0..
0..
1
a[0]
256bit
a[1]
... 0..
1
64bit
b[x+0]
0..
1 1..
1 1..
0
b[x+1] b[x+2] b[x+3]
夏のプログラムシンポジウム2012
メモリを減らして高速化できる?
キャッシュ効率の向上
テーブルは一つにまとめる
256bit単位ではなく512bit単位で集めてみる
64bitで区切ると8個
するとメモリ必要量は(32 + 32 + 8 * 8) / 512 = 1/4
変わらない
128bitで区切ると4個
メモリ必要量は(32 + 8 * 4) / 512 = 1/8
半分になる
問題点
256bitを超えるのでuint8_tな配列b[]の積算でオーバーフローしてしまう
オンデマンドで総和を求める?/ 3326夏のプログラムシンポジウム2012
4個未満のsum()
最適化したいコード(0 <= n < 4)
Xeonで35clk, i7で30lk程度であった(乱数生成込み)
/ 3327
int sum1(const uint8_t data[4], int n) {int sum = 0;for (int i = 0; i < n; i++) {
sum += data[i];}return sum;
}
XorShift128 rg;for (int j = 0; j < C; j++) {
ret += sum1(data, rg.get() % 4);}
夏のプログラムシンポジウム2012
ループ展開してみた
Xeon 35→26clk, i7 30→24clk
Loop Stream Detector(LSD)
/ 3328
int sum2(const uint8_t data[4], int n) {int sum = 0;switch (n) {case 3: sum += data[2];case 2: sum += data[1];case 1: sum += data[0];}return sum;
}
夏のプログラムシンポジウム2012
psadbwを使ってみる
psadbw X, Y
MPEGなどのビデオコーデックにおいて二つのbyte単位のデータの差の絶対値の和を求める命令
psadbw(X, Y) = sum [abs(X[i] – Y[i]) | i = 0..7]
画像の一致度を求める
Y = 0とするとXのbyte単位の和を求められる
Xをマスクして足せばよい
/ 3329
x0 x1 x2 x3 x4 x5 x6 x7
0xff 0xff 0xff 0 0 0 0 0
and
x0 x1 x2 0 0 0 0 0
x0 + x1 + x2夏のプログラムシンポジウム2012
実際のコード
Xeon 35→26→10clk!i7 30→24→10clk!
速くなった
このコードはn < 8まで適用可能
ちょっと改良すればn < 16まで可能
/ 3330
int sum3(const uint8_t data[4], int n) {uint32_t x = *reinterpret_cast<const uint32_t*>(data);x &= (1U << (n * 8)) - 1;V128 v(x);v = psadbw(v, Zero());return movd(v);
}
夏のプログラムシンポジウム2012
128bitマスク
残りはマスクしてpopcntする部分
実際には64bit整数に分解して実行する
n < 63 と n >= 64に場合分けする
夏のプログラムシンポジウム2012 / 3331
// 疑似コードuint128_t mask128(int n) {return (uint128_t(1) << n) – 1; }
int get(uint12_t x, int n) {return popcnt_128(x & mask128(n)); }
uint64_t m0 = -1;uint64_t m1 = 0;if (!(n > 64)) m0 = mask;if (n > 64) m1 = mask;ret += popCount64(b0 & m0);ret += popCount64(b1 & m1);
分岐除去
先程のコードはgcc4.6では条件分岐命令を生成する
データがランダムなら確率50%で分岐予測が外れる
cmovを使って実装
条件が成立したときのみmovを行う命令
if (n > 64)はcarryを変更しなければ一度だけでよい
6clkぐらい速くなる
メモリアクセスの影響が多いと見えにくくなる
夏のプログラムシンポジウム2012 / 3332
or(m0, -1);and(n, 64); // ZF = (n < 64) ? 1 : 0cmovz(m0, mask); // m0 = (!(n > 64)) ? mask : -1cmovz(mask, idx); // mask = (n > 64) ? mask : 0and(m0, ptr [blk + rax * 8 + 0]);and(mask, ptr [blk + rax * 8 + 8]);popcnt(m0, m0);popcnt(rax, mask);
select1のベンチマーク
/ 3333
0
50
100
150
200
128KiB 0.5MiB 2MiB 8MiB 32MiB 0.1GiB 0.5GiB 2GiB 4GiB
rankの処理時間(clk)
SBV1(org) SBV2
fast
メモリを大量に使うところでは速い
marisa-trieのbit-vectorよりも速い
少ないところではオーバーヘッドがややある
まだ改良の余地あり・あるいはnによって戦略を変える
夏のプログラムシンポジウム2012