Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

9,581 views

Published on

intorduction of SSE4.2 string instructions

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

  1. 1. SSE4.2の文字列処理命令の紹介 Cybozu Labs 2011/8/6 光成滋生(8/23加筆修正p3, p.25) x86/x64最適化勉強会#12011/8/6 /41 1
  2. 2. 内容SIMD向き/不向きSSE2によるstrlenSSE4.2の文字列処理命令SSE4.2によるstrlenintrinsic命令単語を数える改良まとめ2011/8/6 2 /41
  3. 3. 説明とコードIntelプロセッサ最適化マニュアルを読もう http://homepage1.nifty.com/herumi/prog/intel-opt.htmlコード片 https://github.com/herumi/opti/アライメントとページ境界に関する補足(必読) http://homepage1.nifty.com/herumi/diary/1108.html#82011/8/6 3 /41
  4. 4. SIMD向き/不向きSIMDは4個(or 8/16)個のデータを同時に同じよう に処理するためのものSIMD向き 先程の最大値を求める場合,4個ずつやってもよかった 複数個同時に加算,掛け算,etc. これらの処理は概ね得意SIMD向きでないもの 本質的に分岐が多いもの 一つ前の状態に依存するもの2011/8/6 4 /41
  5. 5. SSE2によるstrlen素朴なstrlen size_t strlenC(const char *p) { size_t len = 0; while (p[len]) len++; return len; }SIMDを使うアイデア 16byteずつデータを取得してbyte単位で0と比較する 0があれば0xff, なければ0をbyte単位に取得できる 上記データの最上位ビット(MSB)をかき集める 下から数えて初めて1になった場所があればそこが¥02011/8/6 5 /41
  6. 6. SIMDを使うアイデア xm0に文字列, xm1に0を入れておいてbyte比較  pcmpeqb xm0, xm1 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 xm0のMSBをかき集める  pmovmskb eax, xm0  1bitずつ16個合計16bitのデータになる  eax = 0b00100000 eaxが0で無ければ下から初めて1になる場所を探す  bsf eax, eax ; eax = 5となる2011/8/6 6 /41
  7. 7. 実装https://github.com/herumi/opti/のstrlen_sse2.cpp 実際には16byteアライメントされていないと扱いにくいた めループ開始前に端数処理がある 当時3年前(gcc 4.3やVC9に対して)は速かった 最近のgcc(4.4以降?)は同様の手法やこれから述べる SSE4.2の命令が用いられている ただしgcc 4.6でもstrlenCがそういうコードになるわけではない  あくまでもライブラリ関数で使われているだけまとめ アライメント処理 / byte単位での照合 / ビットスキャン2011/8/6 7 /41
  8. 8. SSE4.2の文字列処理命令超複雑 今述べたアライメントの処理(不要になった), byte単位での照合(より高機能), ビットスキャンを1命令で行うpcmpXstrY xm0, xm1/mem, imm8 imm8でさまざまなモードを指定する memは16byte alignmentされていなくてもよい 0終端による文字列 edx/eaxによる長さ指定文字列 処理の結果をスキャンして pcmpistri pcmpestri ecxに出力 処理の結果をそのままxm0に pcmpistrm pcmpestrm 出力2011/8/6 8 /41
  9. 9. 長さ指定モードpcmpestri, pcmpestrmは明示的な長さを指定するpcmpestrY xm0, xm1, imm8 eaxはxm0の文字列の長さ edxはxm1の文字列の長さ2011/8/6 9 /41
  10. 10. 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) 部分文字列比較をする2011/8/6 10 /41
  11. 11. equal anyの疑似コードset : 検索したい文字集合text : 検索対象のテキスト 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; }set = "abc", text = "xaybzc"; IntRes1[0] = setのどれもtext[0]にマッチしないのでfalse IntRes1[1] = setのaがtext[1]にマッチするのでtrue ...2011/8/6 11 /41
  12. 12. rangesの疑似コードstr:[2*i+0] : 文字列範囲の始めstr[2*i+1] : 文字列範囲の終わり 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; }str = "az09", text = "0Abc"; IntRes1[0] = text[0]は[0-9]にあるのでtrue IntRes1[1] = text[1]はどこにも入らないのでfalse IntRes1[2] = text[2]は[a-z]にあるのでtrue2011/8/6 12 /41
  13. 13. equal eachの疑似コードstr : 検索したい文字列text : 検索対象のテキスト for (int j = 0; j < len; j++) { IntRes1[j] = text[j] == str[j]; }str = "abc", text = "aXc"; IntRes1[0] = text[0] == aなのでtrue IntRes1[1] = text[1] != bなのでfalse IntRes1[2] = text[2] == cなのでtrue2011/8/6 13 /41
  14. 14. equal orderedの疑似コードstr : 検索したい文字列text : 検索対象のテキスト 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; }これは複雑なのでinvalid文字列の扱いを説明して から2011/8/6 14 /41
  15. 15. imm8[5:4]中間結果(IntRes1)に作用してIntRes2を作る00b(positive polarity) そのまま出力(IntRes2[i] = IntRes1[i])01b(negative polarity) 反転して出力(IntRes2[i] = ~IntRes1[i])10b(masked(+)) そのまま01b(masked(-)) 入力がinvalidならそのまま,そうでなければ反転2011/8/6 15 /41
  16. 16. 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に入 る.2011/8/6 16 /41
  17. 17. invalid文字の扱いinvalid文字 文字列の比較において,途中に¥0が出たり,長さが短く て終わってしまったときの残りの文字たちのことinvalid文字と通常の文字との演算ルール 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 常に12011/8/6 17 /41
  18. 18. equal orderedの例src = "ABCA¥0XYZ", text = "BABCAB¥0S"; 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 F 文 1 B fF fF T F F T F x T 字 F 列 2 C fF fF F F T F x x F 3 A fF fF F T F x x x F F 4 ¥0 fT fT fT fT fT x x x F 5 X fT fT fT x x x x x F 6 Y fT fT x x x x x x IntRes1 7 Z fT x x x x x x x¥0の後ろはinvalid http://journal.mycom.co.jp/articles/2008/04/10/idf09/008.htmlT:True, F:False, fT:force True, fF:force Flase2011/8/6 18 /41
  19. 19. フラグレジスタフラグレジスタは次のように変化する pcmpXstrY xm0, xm1, imm8 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 19 /41
  20. 20. 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 = 02011/8/6 20 /41
  21. 21. SSE4.2を使ったstrlenよってコードは次のようになる とても簡単 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 21 /41
  22. 22. ベンチマーク ランダムな長さの文字列に対するbyteあたりの処理時間 文字列平均長 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 SSE2バージョンは文字列長が長いと速い  32byte単位で処理しているため glibcとSSE4.2は大体同じ  短いところでglibcが若干速いのは短いとき用の処理が入ってる から2011/8/6 22 /41
  23. 23. 注意 初期のintelのマニュアルは遅いバージョンが載っていた http://journal.mycom.co.jp/articles/2008/04/10/idf09/008.html L("@@"); add(a, ecx); // 16でなくてecx L(".in"); pcmpistri(xm0, ptr [a], 0x14); // 比較して jnz("@b"); これではpcmpistriの出力結果のecxが確定するまでadd で値を計算できないためスループットが下がるため2011/8/6 23 /41
  24. 24. intrinsic版の注意点pcmpistriなどの命令はecx(またはxm0)とフラグの 両方を出力するがintrinsicは一つしか値をとれない 個別の値を取る関数が用意されている 返り値 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 _mm_cmpestraなどはAFを取得するものではない AFは常に0に設定される またhttp://msirocoder.blog35.fc2.com/blog-entry-65.html で触れられているようなResetでも無いように思う(自信無し)2011/8/6 24 /41
  25. 25. SSE4.2 intrinsic版strlen #ifdef _WIN32 *(__m128i*)pはmovdqaが生成される可能性がある #include <intrin.h> _mm_loadu_si128((__m128i*)p)を使うべき #else (他のスライドも同様) cf. #include <x86intrin.h> http://homepage1.nifty.com/herumi/diary/1108.html#8 #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; }msiroさんのwhile()の方が簡潔でよさそう asmコードと実行時間は同じようなものになる2011/8/6 25 /41
  26. 26. 生成コード(by gcc 4.6.0) 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_mm_cmpistrzと_mm_cmpistriの両方が一つの pcmpistriにまとめられている よかった.たいしたもんだ2011/8/6 26 /41
  27. 27. strchr by SSE4.2ナイーブな実装 const char *strchr_C(const char *p, int c) { while (*p) { if (*p == (char)c) return p; p++; } return 0; }imm8の選択 符号無しbyte単位でimm8[1:0] = 0 集計はequal anyでimm8[3:2] = 0 文字列は"(char)c"; 中間結果と最終結果は何もしないimm8[6:5:4] = 02011/8/6 27 /41
  28. 28. 何のフラグでループするかIntRes2 != 0なら文字を発見したのでループ脱出 CFを確認する次に文字列が終了したかをZFで確認する #lp pcmpistri xm0, ptr [p], 0 jc #found jnz #lpCF = 0 && ZF = 0のとき,すなわちjaとすればよい #lp p += 16 pcmpistri xm0, ptr [p], 0 ja #lp jnc #notfound #found2011/8/6 28 /41
  29. 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; } Xeon strchrLIB strchr_C strchrSSE42 clk 0.459 3.012 0.2522011/8/6 29 /41
  30. 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; }if ((unsigned char)(*p - c1) <= (unsigned char)(c2 - c1)) return p; により1割ぐらい早くなるがSSE4.2版はequal anyをrangesにするだけ2011/8/6 30 /41
  31. 31. findRange by SSE4.2 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.2142011/8/6 31 /41
  32. 32. 単語のカウントここでは英数字とアポストロフィの連続を単語とし, その個数を数える(インテルのマニュアルより)インテルの比較用Cコードはかなりトリッキー だが,凄く速いというわけでもない(おもしろいけど) 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 32 /41
  33. 33. 多少最適化したもの今回はこれを使う(2倍程度速い) 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 33 /41
  34. 34. SSE4.2 intrinsc版countWordmsiroさんのを少し変更 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 34 /41
  35. 35. countWord_SSE42の解説, 0-9, A-Z, a-zの範囲にあるもの(C)を探す rangesを使う(C)とそれ以外の境界は0から1, 1から0に変化する エッジは1bitずらしてxorすれば検出できる 1 1 1 0 0 1 0 0 1 1 1 0 0 1 0 0 0次回使う xor 0 0 1 0 1 1 0 0 ビットの立っている個数を数える 倍数えることになるので最後に2で割る2011/8/6 35 /41
  36. 36. SSE4.2 intrinsc版countWord(C)に入る範囲を配列で指定する MIE_ALIGN(16) static const char alnumTbl[16] = { ¥, ¥, 0, 9, A, Z, a, z, ¥0 }; const __m128i im = *(const __m128i*)alnumTbl;符号無しbyte単位なのでimm8[1:0] = 0集計方法はranges imm8[3:2] = 01b中間操作:そのまま出力 imm8[5:4] = 0最終操作:そのまま出力 imm8[6] = 0よってimm8 = 0b100 = 42011/8/6 36 /41
  37. 37. SSE4.2 intrinsc版countWorddo { 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));retの各ビットに(c in (C)) ? 1 : 0が入るx = ret << 1;x = x | prev; // 前回の残りの1bitprev = ret >> 15;x = x ^ ret.count += xのビットの数文字列の中に¥0が見つかるまでループ2011/8/6 37 /41
  38. 38. ベンチマーク i7 インテルオリジナル 改良版 intrinsic版 clk 854 456 42 一桁速い! L("@@"); add(p, 16); 生成コードを見てみる movdqa(xm1, ptr [p]);  あれ,pcmpistrmが2回 pcmpistrm(xm2, xm1, 4); movdqa(xm4, xm0); pcmpistrmはフラグを変える psllw(xm4, 1); por(xm4, xm3);  真ん中のadd(a, d);が邪魔 pxor(xm4, xm0);  lea(a, ptr [a + d]);にすればOK? movd(d, xm4); movdqa(xm3, xm0); popcnt(d, d); add(a, d); psrld(xm3, 15); pcmpistrm(xm2, xm1, 4); jnz("@b");2011/8/6 38 /41
  39. 39. 改良 実はpopcntもフラグをいじる 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"); i7 インテルオリジナル 改良版 intrinsic版 改良版 clk 854 456 42 332011/8/6 39 /41
  40. 40. 改悪? Xeonでは遅くなっていた i7 インテルオリジナル 改良版 intrinsic版 改良版 clk 854 456 42 33 Xeon インテルオリジナル 改良版 intrinsic版 改良版? clk 1714 874 46 53 いろいろ難しい…2011/8/6 40 /41
  41. 41. まとめSSE4.2の命令の紹介 アライメントを気にする必要がない 超高機能な文字マッチパターン bsf, bsr相当の機能も持つ intrinsic版はフラグとecx/xm0の両方必要 手動最適化の余地あり? プロセッサによって結構性能が違うこともある2011/8/6 41 /41

×