33
高機能アセンブラによるx86/x64 CPUけ高速化テクニック Cybozu Labs 2012/8/24 光成滋生

Prosym2012

Embed Size (px)

Citation preview

Page 1: Prosym2012

高機能アセンブラによるx86/x64 CPU向け高速化テクニック

Cybozu Labs

2012/8/24 光成滋生

Page 2: Prosym2012

目次

x86/x64アセンブラの復習

C++用のx86/x64アセンブラXbyakの紹介

サンプル

文字列検索

ビットを数える

自己紹介

x86/x64最適化勉強会主催

https://github.com/herumi/opti/

http://www.slideshare.net/herumi/

/ 332夏のプログラムシンポジウム2012

Page 3: Prosym2012

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

Page 4: Prosym2012

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

Page 5: Prosym2012

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

Page 6: Prosym2012

簡単なサンプル(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

Page 7: Prosym2012

簡単なサンプル(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

Page 8: Prosym2012

簡単なサンプル(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

Page 9: Prosym2012

応用例

ビューティフルコード8章

画像処理のためのその場コード生成

画像処理では関数内では固定のパラメータが多い

templateなどを組み合わせるとバイナリサイズが肥大化

/ 339夏のプログラムシンポジウム2012

Page 10: Prosym2012

画像処理のためのその場コード生成

ビットマップ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

Page 11: Prosym2012

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

Page 12: Prosym2012

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

Page 13: Prosym2012

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

Page 14: Prosym2012

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

Page 15: Prosym2012

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

Page 16: Prosym2012

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

Page 17: Prosym2012

ベンチマーク

対象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

Page 18: Prosym2012

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

Page 19: Prosym2012

考察

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

Page 20: Prosym2012

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

Page 21: Prosym2012

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

Page 22: Prosym2012

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

Page 23: Prosym2012

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

Page 24: Prosym2012

ビットを数える

簡潔データ構造

サイズ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

Page 25: Prosym2012

ナイーブな実装

岡野原さんの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

Page 26: Prosym2012

メモリを減らして高速化できる?

キャッシュ効率の向上

テーブルは一つにまとめる

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

Page 27: Prosym2012

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

Page 28: Prosym2012

ループ展開してみた

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

Page 29: Prosym2012

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

Page 30: Prosym2012

実際のコード

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

Page 31: Prosym2012

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);

Page 32: Prosym2012

分岐除去

先程のコードは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);

Page 33: Prosym2012

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