More Related Content Similar to Prosym2012 (20) More from MITSUNARI Shigeo More from MITSUNARI Shigeo (20) Prosym20123. 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) + <即値>]
例)
mov rax, rcx // rax = rcx;
add ecx, 4 // ecx += 4;
sub r8,[rax+ebx*4+12]//r8 -= *(int64_t)(rax+ebx*4+12);
夏のプログラムシンポジウム2012 3 / 33
4. 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オペランドタイプもある
例)
movaps xmm0,[eax] //xmm0にfloat変数4個が代入される
vaddpd ymm0,ymm3,ymm2 //ymm0 = ymm3 + ymm2(double x 4)
pand xmm2, xmm4 //xmm2 &= xmm4
夏のプログラムシンポジウム2012 4 / 33
5. Xbyakの特長
C++ヘッダのみで記述
外部ライブラリのビルドやリンクが不要
対応コンパイラ : Visual Studio/gcc/clang
対応OS : Windows/Linux/Mac
実行時コード生成
Intel MASM形式に似せたDSLを提供
できるだけ自然に記述できるようにした
アドレッシングは設計時に一番悩んだ部分
かつ一番気に入ってる部分
mov(eax, 5); // mov eax, 5
add(rcx, byte[eax+ecx*4–5]);//add rcx,byte[eax+ecx*4-5]
jmp("lp"); // jmp lp
夏のプログラムシンポジウム2012 5 / 33
6. 簡単なサンプル(1/3)
整数nが与えられたときにnを足す関数を生成
ヘッダをincludeしてCodeGeneratorを継承
#include <xbyak.h>
struct Code : Xbyak::CodeGenerator {
explicit Code(int n) {
mov(eax, ptr [esp + 4]); // 32bit OS
add(eax, n);
// lea(rax, ptr [rcx + n]); // 64bit Windows
// lea(eax, ptr [edi + n]); // 64bit Linux/Mac
ret();
}
};
夏のプログラムシンポジウム2012 6 / 33
7. 簡単なサンプル(2/3)
インスタンスを生成して実行
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 = %dn", i, n, f(i));
}
}
% ./a.out 9
0 + 9 = 9
1 + 9 = 10
2 + 9 = 11
夏のプログラムシンポジウム2012 7 / 33
8. 簡単なサンプル(3/3)
インスタンスを生成して実行
引数が9のときの関数fの中身をデバッガで確認
32bit Windowsで見てみた
mov eax,dword ptr [esp+4]
add eax,9 // ここが即値
ret
引数が3のときの関数fの中身をデバッガで確認
64bit Linuxで見てみた
0x0000000000607000 in ?? ()
1: x/i $pc
0x607000: lea eax,[edi+0x3]
// ここが即値
夏のプログラムシンポジウム2012 8 / 33
9. 応用例
ビューティフルコード8章
画像処理のためのその場コード生成
画像処理では関数内では固定のパラメータが多い
templateなどを組み合わせるとバイナリサイズが肥大化
夏のプログラムシンポジウム2012 9 / 33
10. 画像処理のためのその場コード生成
ビットマップD, Sを合成変換する関数
変換種類を示すopがループの最内部分にあるため通常の実装
では遅すぎる
1985年のWindowsではそのコードを生成するミニコンパイラ
を含んでいたらしい
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 10 / 33
11. Xbyakによるその場コード生成
メリット Code(const Rect& rect, int op){
// generate prolog
C++の文法でasmの制御構造を L(".lp_y");
記述できる mov(rcx, rect.width);
L(".lp_x");
極めて直感的に扱える
switch (op) {
アセンブラ独自の疑似命令を ..
覚える必要がない case 0x60:
mov(eax,ptr[ptrD+rbx]);
Cの構造体との連携が xor(eax,ptr[ptrS+rbx]);
シームレス and(eax,ptr[ptrP+rbx]);
構造体メンバのオフセット break;
取得は外部ツールでは困難 }
mov(ptr[ptrD + rbx], eax);
Xbyakなら<stddef.h>の add(rbx, 4);
offsetof(type, member) sub(rcx, 1);
をそのまま利用可能 jnz(".lp_x");
...
夏のプログラムシンポジウム2012 11 / 33
12. FFTのバタフライ演算(1/2)
ビット反転とデータ移動 void swap(double*a,int k1,int j1){
__m128d x = _mm_load_pd(a + j1);
入力は次数nとデータa __m128d y = _mm_load_pd(a + k1);
ipはビット反転用work __mm_store_pd(a + j1, y);
__mm_store_pd(a + k1, x);
nは可変だが,128とか }
256とかが多いケース void bitrv(int n,int *ip,double *a)
通常は事前に専用コード {
int j, j1, k, k1, l, m, m2;
を用意して分岐する
l = n; m = 1;
ツールでCのコードを生成 ...
するなど 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 12 / 33
13. FFTのバタフライ演算(2/2)
修正箇所はわずか struct Code : Xbyak::CodeGenerator {
swap()をコード生成 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;
movapd xm0,ptr [eax+10h] ...
movapd xm1,ptr [eax+100h] j1 = 2 * j + ip[k];
movapd ptr [eax+10h],xm1 k1 = 2 * k + ip[j];
movapd ptr [eax+100h],xm0 swap(a, j1, k1);
movapd xm0,ptr [eax+50h] ...
movapd xm1,ptr [eax+140h]
...
夏のプログラムシンポジウム2012 13 / 33
14. 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
夏のプログラムシンポジウム2012 14 / 33
15. strstrを作ってみる
16byteずつ文字列を探索する
入力パラメータ
a : 入力テキスト(text)ポインタが格納されたレジスタ
xm0 : 検索文字(の先頭最大16byte)
12 : 文字列マッチをuint8_tの部分文字マッチさせる即値
出力フラグ
CF : 文字列を見つける前にtextが終われば1
ZF : aから16byte内に文字列がなければ0
movdqu(xm0, ptr [key]); // xm0 = *key
L(".lp");
pcmpistri(xm0, ptr [a], 12);
lea(a, ptr [a + 16]);
ja(".lp");
jnc(".notFound");
夏のプログラムシンポジウム2012 15 / 33
16. strstrのコア部分(続き)
keyの先頭文字が一致する部分を検出した
先程のループを抜けたときはcにマッチしたオフセットが入っ
ているのでそれだけ進める(add a, c)
その場所からkeyの最後まで一致しているかを確認
OF == 0なら一致しなかった
SF == 1なら見つかった
add(a, c);
mov(save_a, a); // save a
mov(save_key, key); // save key
L(".tailCmp");
movdqu(xm1, ptr [save_key]);
pcmpistri(xm1, ptr [save_a], 12);
jno(".next"); // if (OF == 0) goto .next
js(".found"); // if (SF == 1) goto .found
add(save_a, 16);
add(save_key, 16);
jmp(".tailCmp");
夏のプログラムシンポジウム2012 16 / 33
17. ベンチマーク
対象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数を表示する
夏のプログラムシンポジウム2012 17 / 33
18. strstrのベンチマーク
Xeon X5650 + gcc 4.6.3
8 string::find
7 boost::bm
fast
cycle/Byte to find
6 quick search
5 strstr(gcc)
4 my_strstr
3
2
1
0
static_ass
a ab 1234 これは 00...0 AB...Z
ert
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
夏のプログラムシンポジウム2012 18 / 33
19. 考察
ABC....Zの様なBM法やQuick Searchアルゴリズムにと
って有利な文字列すらstrstrより遅い
BM法やQuick Searchはテーブルを引いてオフセットを足すた
めパイプラインに悪影響がある
通常の文字列検索では出番がない?
// 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 19 / 33
20. strcasestrの実装
keyの大文字小文字を区別しないstrstr
コードの簡略さのためにkeyは事前に小文字化しておく
大文字小文字を区別しないマッチ方法
textを小文字にしてマッチさせる(先程のコードに委譲)
movdqu(xm0, ptr [key]); // xm0 = *key
L(".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 .lp
jnc(".notFound");
夏のプログラムシンポジウム2012 20 / 33
21. 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';を書き換える
('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 21 / 33
22. toLower(2/2)
実際のコード片
/*
toLower in x
Am1 : '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' - 1
movdqa(t1, Zp1);
pcmpgtb(t1, x); // -1 if 'Z' + 1 > c
pand(t0, t1); // -1 if [A-Z]
pand(t0, amA); // 0x20 if c in [A-Z]
paddb(x, t0); // [A-Z] -> [a-z]
}
夏のプログラムシンポジウム2012 22 / 33
23. CPU判別によるディスパッチ
コア部分再度(詳細) pcmpistri(xm0,ptr[a],12);
実験によるとCPUの世代で if (isSandyBridge) {
コードの書き方で速度が違う lea(a, ptr [a + 16]);
(lea vs add) ja(".lp");
} else {
実行時にCPU判別することで
jbe(".headCmp");
適切なコード生成を行う
add(a, 16);
10%程度速度向上があった jmp(".lp");
L(".headCmp");
}
jnc(".notFound");
if (isSandyBridge) {
lea(a,ptr[a+c-16]);
} else {
add(a, c); }
夏のプログラムシンポジウム2012 23 / 33
24. ビットを数える
簡潔データ構造
サイズ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の存在は前提
夏のプログラムシンポジウム2012 24 / 33
25. ナイーブな実装
岡野原さんの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);
ランダムアクセスするのでキャッシュミスが頻出
. . . . . . . .
0 1 1 0 0 1 0 1 1 0 0 1 ... 0 1 0 1 1 1 1 0
. . . . . . . .
256bit 64bit
a[0] a[1] b[x+0] b[x+1] b[x+2] b[x+3]
夏のプログラムシンポジウム2012 25 / 33
26. メモリを減らして高速化できる?
キャッシュ効率の向上
テーブルは一つにまとめる
256bit単位ではなく512bit単位で集めてみる
64bitで区切ると8個
するとメモリ必要量は(32 + 32 + 8 * 8) / 512 = 1/4
変わらない
128bitで区切ると4個
メモリ必要量は(32 + 8 * 4) / 512 = 1/8
半分になる
問題点
256bitを超えるのでuint8_tな配列b[]の積算で
オーバーフローしてしまう
オンデマンドで総和を求める?
夏のプログラムシンポジウム2012 26 / 33
27. 4個未満のsum()
最適化したいコード(0 <= n < 4)
Xeonで35clk, i7で30lk程度であった(乱数生成込み)
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 27 / 33
28. ループ展開してみた
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;
}
Xeon 35→26clk, i7 30→24clk
Loop Stream Detector(LSD)
夏のプログラムシンポジウム2012 28 / 33
29. psadbwを使ってみる
psadbw X, Y
MPEGなどのビデオコーデックにおいて
二つのbyte単位のデータの差の絶対値の和を求める命令
psadbw(X, Y) = sum [abs(X[i] – Y[i]) | i = 0..7]
画像の一致度を求める
Y = 0とするとXのbyte単位の和を求められる
Xをマスクして足せばよい
x0 x1 x2 x3 x4 x5 x6 x7
and
0xff 0xff 0xff 0 0 0 0 0
x0 x1 x2 0 0 0 0 0
夏のプログラムシンポジウム2012
x0 + x1 + x2 29 / 33
30. 実際のコード
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);
}
Xeon 35→26→10clk!
i7 30→24→10clk!
速くなった
このコードはn < 8まで適用可能
ちょっと改良すればn < 16まで可能
夏のプログラムシンポジウム2012 30 / 33
31. 128bitマスク
残りはマスクしてpopcntする部分
// 疑似コード
uint128_t mask128(int n) {
return (uint128_t(1) << n) – 1; }
int get(uint12_t x, int n) {
return popcnt_128(x & mask128(n)); }
実際には64bit整数に分解して実行する
n < 63 と n >= 64に場合分けする
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);
夏のプログラムシンポジウム2012 31 / 33
32. 分岐除去
先程のコードはgcc4.6では条件分岐命令を生成する
データがランダムなら確率50%で分岐予測が外れる
cmovを使って実装
条件が成立したときのみmovを行う命令
if (n > 64)はcarryを変更しなければ一度だけでよい
6clkぐらい速くなる
メモリアクセスの影響が多いと見えにくくなる
or(m0, -1);
and(n, 64); // ZF = (n < 64) ? 1 : 0
cmovz(m0, mask); // m0 = (!(n > 64)) ? mask : -1
cmovz(mask, idx); // mask = (n > 64) ? mask : 0
and(m0, ptr [blk + rax * 8 + 0]);
and(mask, ptr [blk + rax * 8 + 8]);
popcnt(m0, m0);
popcnt(rax, mask);
夏のプログラムシンポジウム2012 32 / 33
33. select1のベンチマーク
メモリを大量に使うところでは速い
marisa-trieのbit-vectorよりも速い
少ないところではオーバーヘッドがややある
まだ改良の余地あり・あるいはnによって戦略を変える
rankの処理時間(clk) fast
200
150
100
50
0
128KiB 0.5MiB 2MiB 8MiB 32MiB 0.1GiB 0.5GiB 2GiB 4GiB
SBV1(org) SBV2
夏のプログラムシンポジウム2012 33 / 33