SSE4.2の文字列処理命令の紹介

41
SSE4.2の文字列処理命令の紹介 SSE4.2の文字列処理命令の紹介 Cybozu Labs 2011/8/6 光成滋生(8/23加筆修正p3, p.25) x86/x64最適化勉強会#1 2011/8/6 /41 1

description

intorduction of SSE4.2 string instructions

Transcript of SSE4.2の文字列処理命令の紹介

Page 1: SSE4.2の文字列処理命令の紹介

SSE4.2の文字列処理命令の紹介 SSE4.2の文字列処理命令の紹介

Cybozu Labs

2011/8/6 光成滋生(8/23加筆修正p3, p.25)

x86/x64最適化勉強会#1

2011/8/6 /41 1

Page 2: SSE4.2の文字列処理命令の紹介

内容 内容

SIMD向き/不向き

SSE2によるstrlen

SSE4.2の文字列処理命令

SSE4.2によるstrlen

intrinsic命令

単語を数える

改良

まとめ

/41 2 2011/8/6

Page 3: SSE4.2の文字列処理命令の紹介

説明とコード 説明とコード

Intelプロセッサ最適化マニュアルを読もう

http://homepage1.nifty.com/herumi/prog/intel-opt.html

コード片

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

アライメントとページ境界に関する補足(必読)

http://homepage1.nifty.com/herumi/diary/1108.html#8

/41 3 2011/8/6

Page 4: SSE4.2の文字列処理命令の紹介

SIMD向き/不向き SIMD向き/不向き

SIMDは4個(or 8/16)個のデータを同時に同じように処理するためのもの

SIMD向き

先程の最大値を求める場合,4個ずつやってもよかった

複数個同時に加算,掛け算,etc.

これらの処理は概ね得意

SIMD向きでないもの

本質的に分岐が多いもの

一つ前の状態に依存するもの

/41 4 2011/8/6

Page 5: SSE4.2の文字列処理命令の紹介

SSE2によるstrlen SSE2によるstrlen

素朴なstrlen

SIMDを使うアイデア

16byteずつデータを取得してbyte単位で0と比較する

0があれば0xff, なければ0をbyte単位に取得できる

上記データの最上位ビット(MSB)をかき集める

下から数えて初めて1になった場所があればそこが'¥0'

/41 5

size_t strlenC(const char *p) { size_t len = 0; while (p[len]) len++; return len; }

2011/8/6

Page 6: SSE4.2の文字列処理命令の紹介

xm0に文字列, xm1に0を入れておいてbyte比較

pcmpeqb xm0, xm1

xm0のMSBをかき集める

pmovmskb eax, xm0

1bitずつ16個合計16bitのデータになる

eax = 0b00100000

eaxが0で無ければ下から初めて1になる場所を探す

bsf eax, eax ; eax = 5となる

SIMDを使うアイデア SIMDを使うアイデア

xm0 +0 +1 +2 +3 +4 +5 +6 +7

xm0 h e l l o '¥0' ? ?

xm1 0 0 0 0 0 0 0 0

xm0 0 0 0 0 0 0xff 0 0

/41 6 2011/8/6

Page 7: SSE4.2の文字列処理命令の紹介

実装 実装

https://github.com/herumi/opti/のstrlen_sse2.cpp

実際には16byteアライメントされていないと扱いにくいためループ開始前に端数処理がある

当時3年前(gcc 4.3やVC9に対して)は速かった

最近のgcc(4.4以降?)は同様の手法やこれから述べるSSE4.2の命令が用いられている

ただしgcc 4.6でもstrlenCがそういうコードになるわけではない

あくまでもライブラリ関数で使われているだけ

まとめ

アライメント処理 / byte単位での照合 / ビットスキャン

/41 7 2011/8/6

Page 8: SSE4.2の文字列処理命令の紹介

SSE4.2の文字列処理命令 SSE4.2の文字列処理命令

超複雑

今述べたアライメントの処理(不要になった), byte単位での照合(より高機能), ビットスキャンを1命令で行う

pcmpXstrY xm0, xm1/mem, imm8

imm8でさまざまなモードを指定する

memは16byte alignmentされていなくてもよい

/41 8

0終端による文字列 edx/eaxによる長さ指定文字列

処理の結果をスキャンしてecxに出力

pcmpistri pcmpestri

処理の結果をそのままxm0に出力

pcmpistrm pcmpestrm

2011/8/6

Page 9: SSE4.2の文字列処理命令の紹介

長さ指定モード 長さ指定モード

pcmpestri, pcmpestrmは明示的な長さを指定する

pcmpestrY xm0, xm1, imm8

eaxはxm0の文字列の長さ

edxはxm1の文字列の長さ

/41 9 2011/8/6

Page 10: SSE4.2の文字列処理命令の紹介

imm8の内容 imm8の内容

imm8[0](最下位ビット)

0なら入力データをbyte単位とみなす 1ならword(2byte)単位と見なして処理

imm8[1]

0なら符号無しデータ 1なら符号ありデータとして処理

imm8[3:2]

文字列の比較方法を選択する

00b(equal array) マッチする最初の文字を探す

01b(ranges) 範囲内から文字列を探す

10b(equal each) 文字列比較をする

11b(equal ordered) 部分文字列比較をする /41 10 2011/8/6

Page 11: SSE4.2の文字列処理命令の紹介

equal anyの疑似コード equal anyの疑似コード

set : 検索したい文字集合

text : 検索対象のテキスト

set = "abc", text = "xaybzc";

IntRes1[0] = setのどれもtext[0]にマッチしないのでfalse

IntRes1[1] = setのaがtext[1]にマッチするのでtrue

...

/41 11

for (int j = 0; j < len; j++) { int tmp = 0; for (int i = 0; i < len; i++) { tmp |= text[j] == set[i]; } IntRes1[j] = tmp; }

2011/8/6

Page 12: SSE4.2の文字列処理命令の紹介

rangesの疑似コード rangesの疑似コード

str:[2*i+0] : 文字列範囲の始め

str[2*i+1] : 文字列範囲の終わり

str = "az09", text = "0Abc";

IntRes1[0] = text[0]は[0-9]にあるのでtrue

IntRes1[1] = text[1]はどこにも入らないのでfalse

IntRes1[2] = text[2]は[a-z]にあるのでtrue

/41 12

for (int j = 0; j < len; j++) { int tmp = 0; for (int i = 0; i < len; i += 2) { tmp |= (str[i] <= text[j]) && (text[j] <= str[i + 1]); } IntRes1[j] = tmp; }

2011/8/6

Page 13: SSE4.2の文字列処理命令の紹介

equal eachの疑似コード equal eachの疑似コード

str : 検索したい文字列

text : 検索対象のテキスト

str = "abc", text = "aXc";

IntRes1[0] = text[0] == aなのでtrue

IntRes1[1] = text[1] != bなのでfalse

IntRes1[2] = text[2] == cなのでtrue

/41 13

for (int j = 0; j < len; j++) { IntRes1[j] = text[j] == str[j]; }

2011/8/6

Page 14: SSE4.2の文字列処理命令の紹介

equal orderedの疑似コード equal orderedの疑似コード

str : 検索したい文字列

text : 検索対象のテキスト

これは複雑なのでinvalid文字列の扱いを説明してから

/41 14

for (int j = 0; j < len; j++) { int tmp = 1; for (int i = 0; i < len - j; i++) { tmp &= text[j + i] == str[i]; } IntRes1[j] = tmp; }

2011/8/6

Page 15: SSE4.2の文字列処理命令の紹介

imm8[5:4] imm8[5:4]

中間結果(IntRes1)に作用してIntRes2を作る

00b(positive polarity)

そのまま出力(IntRes2[i] = IntRes1[i])

01b(negative polarity)

反転して出力(IntRes2[i] = ~IntRes1[i])

10b(masked(+))

そのまま

01b(masked(-))

入力がinvalidならそのまま,そうでなければ反転

/41 15 2011/8/6

Page 16: SSE4.2の文字列処理命令の紹介

imm8[6] imm8[6]

最終結果操作

IntRes2からecxかxm0に出力するデータを作る

pcmpestri, pcmpistriに対する設定

0ならIntRes2のLSBが入る

1ならIntRes2のMSBが入る

ただしIntRes2 == 0なら16(byteのとき)か8(wordのとき)

pcmpestrm, pcmpistrmに対する設定

0ならIntRes2が0拡張されてxmm0に入る.

1ならIntRes2の各ビットが入力データの型の大きさにしたがってbyteかword単位のマスクに拡張されてxmm0に入る.

/41 16 2011/8/6

Page 17: SSE4.2の文字列処理命令の紹介

invalid文字の扱い invalid文字の扱い

invalid文字

文字列の比較において,途中に'¥0'が出たり,長さが短くて終わってしまったときの残りの文字たちのこと

invalid文字と通常の文字との演算ルール

/41 17

xm1(str) xm2(text) equal any ranges equal each equal

orderd

valid valid 通常通り 通常通り 通常通り 通常通り

valid invalid 常に0 常に0 常に0 常に0

invalid valid 常に0 常に0 常に0 常に1

invalid invalid 常に0 常に0 常に1 常に1

2011/8/6

Page 18: SSE4.2の文字列処理命令の紹介

equal orderedの例 equal orderedの例

src = "ABCA¥0XYZ", text = "BABCAB¥0S";

'¥0'の後ろはinvalid

T:True, F:False, fT:force True, fF:force Flase /41 18

text文字列

7 6 5 4 3 2 1 0(j)

S ¥0 B A C B A B

src

文字列

0(i) A fF fF F T F F T F

1 B fF fF T F F T F x

2 C fF fF F F T F x x

3 A fF fF F T F x x x

4 ¥0 fT fT fT fT fT x x x

5 X fT fT fT x x x x x

6 Y fT fT x x x x x x

7 Z fT x x x x x x x

F T F F F F F F

IntRes1

2011/8/6

http://journal.mycom.co.jp/articles/2008/04/10/idf09/008.html

Page 19: SSE4.2の文字列処理命令の紹介

フラグレジスタ フラグレジスタ

フラグレジスタは次のように変化する

pcmpXstrY xm0, xm1, imm8

/41 19

pcmpestri / pcmpestrm pcmpistri / pcmpistrm

CF IntRes2 != 0

ZF |edx| < 16 xm1のいずれかが0なら1, そうでなければ0

SF |eax| < 16 xm0のいずれかが0なら1, そうでなければ0

OF IntRes2[0]

2011/8/6

Page 20: SSE4.2の文字列処理命令の紹介

SSE4.2を使ったstrlen SSE4.2を使ったstrlen

pcmpistriを選択

0終端文字列を扱い,位置をビットスキャンで取る

imm8[1:0] = 0

符号無しbyte単位

'¥0'を探す

1~255にマッチしないものを見つけると考える

範囲指定なのでrangesを使う(imm8[3:2] = 01b)

中間結果操作でビット反転をするのでimm8[5:4]=01b

最終結果の下から初めての1を見つけるのでimm8[6]=0

よってimm8=0b010100=0x14となる

16byteの中に'¥0'がなければZF = 0 /41 20 2011/8/6

Page 21: SSE4.2の文字列処理命令の紹介

SSE4.2を使ったstrlen SSE4.2を使ったstrlen

よってコードは次のようになる

とても簡単

/41 21

mov(eax, 0xff01); // { '0x01', '0xff', '¥0' }; movd(xm0, eax); // xm0にrangesの文字列を設定 mov(a, p); // 文字列の先頭 jmp(".in"); L("@@"); add(a, 16); L(".in"); pcmpistri(xm0, ptr [a], 0x14); // 比較して jnz("@b"); add(a, c); sub(a, p); ret();

2011/8/6

Page 22: SSE4.2の文字列処理命令の紹介

ランダムな長さの文字列に対するbyteあたりの処理時間

SSE2バージョンは文字列長が長いと速い

32byte単位で処理しているため

glibcとSSE4.2は大体同じ

短いところでglibcが若干速いのは短いとき用の処理が入ってるから

ベンチマーク ベンチマーク

文字列平均長 5.04 66.62 261.78 1063.83

glibc 5.70 0.58 0.27 0.20

strlenC 6.92 1.89 1.41 1.24

SSE2 5.74 0.54 0.21 0.15

SSE4.2 5.03 0.69 0.29 0.21

/41 22 2011/8/6

Page 23: SSE4.2の文字列処理命令の紹介

注意 注意

初期のintelのマニュアルは遅いバージョンが載っていた

http://journal.mycom.co.jp/articles/2008/04/10/idf09/008.html

これではpcmpistriの出力結果のecxが確定するまでaddで値を計算できないためスループットが下がるため

/41 23

L("@@"); add(a, ecx); // 16でなくてecx L(".in"); pcmpistri(xm0, ptr [a], 0x14); // 比較して jnz("@b");

2011/8/6

Page 24: SSE4.2の文字列処理命令の紹介

intrinsic版の注意点 intrinsic版の注意点

pcmpistriなどの命令はecx(またはxm0)とフラグの両方を出力するがintrinsicは一つしか値をとれない

個別の値を取る関数が用意されている

_mm_cmpestraなどはAFを取得するものではない

AFは常に0に設定される

またhttp://msirocoder.blog35.fc2.com/blog-entry-65.html で触れられているようなResetでも無いように思う(自信無し)

/41 24

返り値 pcmpestri pcmpestrm pcmpistri pcmpistrm

ecx/xmm0 _mm_cmpestri _mm_cmpestrm _mm_cmpistri _mm_cmpistrm

CF = 0 && ZF = 0 _mm_cmpestra _mm_cmpistra

CF _mm_cmpestrc _mm_cmpistrc

OF _mm_cmpestro _mm_cmpistro

SF _mm_cmpestrs _mm_cmpistrs

ZF _mm_cmpestrz _mm_cmpistrz

2011/8/6

Page 25: SSE4.2の文字列処理命令の紹介

SSE4.2 intrinsic版strlen SSE4.2 intrinsic版strlen

msiroさんのwhile()の方が簡潔でよさそう

asmコードと実行時間は同じようなものになる /41 25

#ifdef _WIN32 #include <intrin.h> #else #include <x86intrin.h> #endif size_t strlenSSE42_C(const char* top) { const __m128i im = _mm_set1_epi32(0xff01); const char *p = top - 16; do { p += 16; } while (!_mm_cmpistrz(im, *(__m128i*)p, 0x14)); // ZF p += _mm_cmpistri(im, *(__m128i*)p, 0x14); // get ecx return p - top; }

2011/8/6

*(__m128i*)pはmovdqaが生成される可能性がある

_mm_loadu_si128((__m128i*)p)を使うべき

(他のスライドも同様) cf.

http://homepage1.nifty.com/herumi/diary/1108.html#8

Page 26: SSE4.2の文字列処理命令の紹介

生成コード(by gcc 4.6.0) 生成コード(by gcc 4.6.0)

_mm_cmpistrzと_mm_cmpistriの両方が一つのpcmpistriにまとめられている

よかった.たいしたもんだ

/41 26

lea rdx, [rdi-16] movdqa xmm0, XMMWORD PTR .LC0[rip] jmp .L11 // align16 .L12: mov rdx, rax .L11: lea rax, [rdx+16] pcmpistri xmm0, XMMWORD PTR [rdx+16], 20 jne .L12

2011/8/6

Page 27: SSE4.2の文字列処理命令の紹介

strchr by SSE4.2 strchr by SSE4.2

ナイーブな実装

imm8の選択

符号無しbyte単位でimm8[1:0] = 0

集計はequal anyでimm8[3:2] = 0

文字列は"(char)c";

中間結果と最終結果は何もしないimm8[6:5:4] = 0

/41 27

const char *strchr_C(const char *p, int c) { while (*p) { if (*p == (char)c) return p; p++; } return 0; }

2011/8/6

Page 28: SSE4.2の文字列処理命令の紹介

何のフラグでループするか 何のフラグでループするか

IntRes2 != 0なら文字を発見したのでループ脱出

CFを確認する

次に文字列が終了したかをZFで確認する

CF = 0 && ZF = 0のとき,すなわちjaとすればよい

/41 28

#lp pcmpistri xm0, ptr [p], 0 jc #found jnz #lp

#lp p += 16 pcmpistri xm0, ptr [p], 0 ja #lp jnc #notfound #found

2011/8/6

Page 29: SSE4.2の文字列処理命令の紹介

コードとベンチマーク コードとベンチマーク

Xeon strchrLIB strchr_C strchrSSE42

clk 0.459 3.012 0.252

/41 29

const char *strchrSSE42_C(const char* p, int c) { const __m128i im = _mm_set1_epi32(c & 0xff); while (_mm_cmpistra(im, *(const __m128i*)p, 0)) { p += 16; } if (_mm_cmpistrc(im, *(const __m128i*)p, 0)) { return p + _mm_cmpistri(im, *(const __m128i*)p, 0); } return 0; }

2011/8/6

Page 30: SSE4.2の文字列処理命令の紹介

範囲指定への拡張 範囲指定への拡張

if ((unsigned char)(*p - c1) <= (unsigned char)(c2 - c1)) return p; により1割ぐらい早くなるが

SSE4.2版はequal anyをrangesにするだけ /41 30

const char *findRange_C(const char* p, char c1, char c2) { const unsigned char *up = (const unsigned char *)p; unsigned char uc1 = c1; unsigned char uc2 = c2; while (*up) { if (uc1 <= *up && *up <= uc2) return (const char*)up; up++; } return 0; }

2011/8/6

Page 31: SSE4.2の文字列処理命令の紹介

findRange by SSE4.2 findRange by SSE4.2

/41 31

const char *findRange_SSE42(const char* p, char c1, char c2) { const __m128i im = _mm_set1_epi32( ((unsigned char)c1) | (((unsigned char)c2) << 8) ); while (_mm_cmpistra(im, *(const __m128i*)p, 4)) { p += 16; } if (_mm_cmpistrc(im, *(const __m128i*)p, 4)) { return p + _mm_cmpistri(im, *(const __m128i*)p, 4); } return 0; }

i7 findRange_C findRange2_C findRange_SSE42

clk 2.310 2.055 0.214

2011/8/6

Page 32: SSE4.2の文字列処理命令の紹介

単語のカウント 単語のカウント

ここでは英数字とアポストロフィの連続を単語とし,その個数を数える(インテルのマニュアルより)

インテルの比較用Cコードはかなりトリッキー

だが,凄く速いというわけでもない(おもしろいけど)

/41 32

size_t countWord_C(const char *p) { static const char alp_map8[32] = { 0, 0, 0, 0, 0x80, 0, 0xff, 0x3, 0xfe, 0xff, 0xff, 0x7, 0xfe, 0xff, 0xff, 0x7 }; size_t i = 1, cnt = 0; unsigned char cc, cc2; bool flag[3]; cc2 = cc = p[0]; flag[1] = alp_map8[cc >> 3] & (1 << (cc & 7)); while (cc2) { cc2 = p[i]; flag[2] = alp_map8[cc2 >> 3] & (1 << (cc2 & 7)); if (!flag[2] && flag[1]) cnt++; flag[1] = flag[2]; i++; } return cnt; }

2011/8/6

Page 33: SSE4.2の文字列処理命令の紹介

多少最適化したもの 多少最適化したもの

今回はこれを使う(2倍程度速い)

/41 33

static char alnumTbl2[256]; // 単語になる文字だけ1, それ以外は0 size_t countWord_C2(const char *p){ size_t count = 0; unsigned char c = *p++; char prev = alnumTbl2[c]; while (c) { c = *p++; char cur = alnumTbl2[c]; if (!cur && prev) { count++; } prev = cur; } return count; }

2011/8/6

Page 34: SSE4.2の文字列処理命令の紹介

SSE4.2 intrinsc版countWord SSE4.2 intrinsc版countWord

msiroさんのを少し変更

/41 34

MIE_ALIGN(16) static const char alnumTbl[16] = { '¥'', '¥'', '0', '9', 'A', 'Z', 'a', 'z', '¥0' }; size_t countWord_SSE42(const char *p) { const __m128i im = *(const __m128i*)alnumTbl; __m128i ret, x, prev = _mm_setzero_si128(); size_t count = 0; goto SKIP; do { p += 16; SKIP: ret = _mm_cmpistrm(im, *(const __m128i*)p, 0x4); x = _mm_slli_epi16(ret, 1); x = _mm_or_si128(prev, x); prev = _mm_srli_epi32(ret, 15); x = _mm_xor_si128(x, ret); count += _mm_popcnt_u32(_mm_cvtsi128_si32(x)); } while (!_mm_cmpistrz(im, *(const __m128i*)p, 0x4)); return count / 2; }

2011/8/6

Page 35: SSE4.2の文字列処理命令の紹介

countWord_SSE42の解説 countWord_SSE42の解説

', 0-9, A-Z, a-zの範囲にあるもの(C)を探す

rangesを使う

(C)とそれ以外の境界は0から1, 1から0に変化する

エッジは1bitずらしてxorすれば検出できる

/41 35

1 1 1 0 0 1 0 0

1 1 1 0 0 1 0 0 0

次回使う 0 0 1 0 1 1 0 0

xor

ビットの立っている個数を数える

倍数えることになるので最後に2で割る

2011/8/6

Page 36: SSE4.2の文字列処理命令の紹介

SSE4.2 intrinsc版countWord SSE4.2 intrinsc版countWord

(C)に入る範囲を配列で指定する

符号無しbyte単位なのでimm8[1:0] = 0

集計方法はranges imm8[3:2] = 01b

中間操作:そのまま出力 imm8[5:4] = 0

最終操作:そのまま出力 imm8[6] = 0

よってimm8 = 0b100 = 4

/41 36

MIE_ALIGN(16) static const char alnumTbl[16] = { '¥'', '¥'', '0', '9', 'A', 'Z', 'a', 'z', '¥0' }; const __m128i im = *(const __m128i*)alnumTbl;

2011/8/6

Page 37: SSE4.2の文字列処理命令の紹介

SSE4.2 intrinsc版countWord SSE4.2 intrinsc版countWord

retの各ビットに(c in (C)) ? 1 : 0が入る

x = ret << 1;

x = x | prev; // 前回の残りの1bit

prev = ret >> 15;

x = x ^ ret.

count += xのビットの数

文字列の中に'¥0'が見つかるまでループ /41 37

do { p += 16; ret = _mm_cmpistrm(im, *(const __m128i*)p, 0x4); x = _mm_slli_epi16(ret, 1); x = _mm_or_si128(prev, x); prev = _mm_srli_epi32(ret, 15); x = _mm_xor_si128(x, ret); count += _mm_popcnt_u32(_mm_cvtsi128_si32(x)); } while (!_mm_cmpistrz(im, *(const __m128i*)p, 0x4));

2011/8/6

Page 38: SSE4.2の文字列処理命令の紹介

ベンチマーク ベンチマーク

i7 インテルオリジナル 改良版 intrinsic版

clk 854 456 42

/41 38

一桁速い!

生成コードを見てみる

あれ,pcmpistrmが2回

pcmpistrmはフラグを変える

真ん中のadd(a, d);が邪魔

lea(a, ptr [a + d]);にすればOK?

L("@@"); add(p, 16); movdqa(xm1, ptr [p]); pcmpistrm(xm2, xm1, 4); movdqa(xm4, xm0); psllw(xm4, 1); por(xm4, xm3); pxor(xm4, xm0); movd(d, xm4); movdqa(xm3, xm0); popcnt(d, d); add(a, d); psrld(xm3, 15); pcmpistrm(xm2, xm1, 4); jnz("@b");

2011/8/6

Page 39: SSE4.2の文字列処理命令の紹介

実はpopcntもフラグをいじる

命令の順序を入れ換えてしまおう

速くなった!

改良 改良

i7 インテルオリジナル 改良版 intrinsic版 改良版

clk 854 456 42 33

/41 39

L("@@"); movdqa(xm4, xm0); psllw(xm4, 1); por(xm4, xm3); movdqa(xm3, xm0); pxor(xm4, xm0); psrld(xm3, 15); movd(d, xm4); popcnt(d, d); add(p, 16); add(a, d); pcmpistrm(xm2, ptr [p], 4); jnz("@b");

2011/8/6

Page 40: SSE4.2の文字列処理命令の紹介

Xeonでは遅くなっていた

いろいろ難しい…

改悪? 改悪?

i7 インテルオリジナル 改良版 intrinsic版 改良版

clk 854 456 42 33

/41 40

Xeon インテルオリジナル 改良版 intrinsic版 改良版?

clk 1714 874 46 53

2011/8/6

Page 41: SSE4.2の文字列処理命令の紹介

まとめ まとめ

SSE4.2の命令の紹介

アライメントを気にする必要がない

超高機能な文字マッチパターン

bsf, bsr相当の機能も持つ

intrinsic版はフラグとecx/xm0の両方必要

手動最適化の余地あり?

プロセッサによって結構性能が違うこともある

/41 41 2011/8/6