« x64(64bit)対応JITアセンブラXbyakリリース | メイン

fast strlen and memchr by SSE2

strlen()とmemchr()のSIMD版を作ってみました.

今回は最速よりもお手軽さを重視したのでアセンブリ言語ではなくintrinsic関数を使っています.そのためVisual Studio 2008, gcc 4.xの両方でコンパイルでき32-bit, 64-bit OS上で動作します.
WindowsとLinuxでのみ確認していますが恐らくIntel Mac OS X上でも動作するでしょう(sample source).

ベンチマークはランダムな長さの文字列の平均長(average length)を変化させつつ取りました.数値は1byteあたりにかかった処理時間比で小さいほど速いことを表します.
strlenが3種類(ANSI, BLOG, SSE2)とmemchrが2種類(ANSI, SSE2)あります.BLOGというのは今回試してみようというきっかけになったCounting Characters in UTF-8 Strings Is Fastの中のCバージョンです.

結果は極端に短いところではSIMDが使えず,かつそのチェックのためのオーバーヘッドがありそれほど速くなりませんが,32文字以上ではは2~5倍高速であることが分かります.
なお,gccのstrlen()は今となっては遅い命令を使っているためよくありません(なぜこうなっているのかちょっと理解に苦しみます).

一つ細かい注意点としては,高速化のために与えられた範囲外の空間を少しアクセスします(readはするがその値は捨てます).16byteアライメントされたところを超えることはないのでページングエラーにはなりませんが,purifyなどのツールを使うと警告がでるかもしれません.


intrinsic関数はVCとgccとで共通化され,しかも32bit, 64bit両対応なため汎用性も高いです.strlenSSE2のように30行未満の関数でも十分は効果はでます.利用しない手はないと思われます.

size_t strlenSSE2(const char *p)
{
    const char *const top = p;
    __m128i c16 = _mm_set1_epi8(0);
    /* 16 byte alignment */
    size_t ip = reinterpret_cast<size_t>(p);
    size_t n = ip & 15;
    if (n > 0) {
        ip &= ~15;
        __m128i x = *(const __m128i*)ip;
        __m128i a = _mm_cmpeq_epi8(x, c16);
        unsigned int mask = _mm_movemask_epi8(a);
        mask &= -(1 << n);
        if (mask) {
            return bsf(mask) - n;
        }
        p += 16 - n;
    }
    for (;;) {
        __m128i x = *(const __m128i*)&p[0];
        __m128i y = *(const __m128i*)&p[16];
        __m128i a = _mm_cmpeq_epi8(x, c16);
        __m128i b = _mm_cmpeq_epi8(y, c16);
        unsigned int mask = (_mm_movemask_epi8(b) << 16) | _mm_movemask_epi8(a);
        if (mask) {
            return p + bsf(mask) - top;
        }
        p += 32;
    }
}
Core2Duo 1.8GHz Xp SP3 + Visual Studio 2008(32bit)
average length257 10 12 16 20 32 64128256512 1024
strlenANSI683.8406.3308.8234.5203.3168.0148.3113.3 86.0 70.5 62.5 58.5 54.5
strlenBLOG835.8449.3347.5269.5234.3195.3175.8140.5109.389.882.078.382.3
strlenSSE2765.8355.5269.5199.3172.0133.0109.5 78.3 47.0 27.3 19.8 15.57.8
memchrANSI1046.8648.5515.8390.5347.8273.3226.5164.0105.5 74.3 62.5 50.8 50.8
memchrSSE2773.5375.0285.0214.8179.5144.8121.3 82.0 54.5 31.3 19.5 15.8 11.8
Core2Duo 1.8GHz Linux 2.4 on VMware + gcc 4.3.0(32bit)
average length2571012162032641282565121024
strlenANSI2019.6933.9728.7576.8512.3430.5386.6316.5261.2235.6219.4214.0212.9
strlenBLOG692.6397.4331.0242.5216.7194.3152.2124.7110.381.576.382.270.0
strlenSSE2560.0275.6214.3159.2135.7104.487.565.141.525.316.614.09.3
memchrANSI1152.4609.5487.4375.0325.6260.5229.9152.495.572.856.849.348.0
memchrSSE2574.6282.1224.3161.0139.1108.590.063.345.123.915.810.011.3
Core2Duo 1.8GHz Linux 2.6.18-53 on VMware + gcc 4.1.2(64bit)
average length257 10 12 16 20 32 64128256512 1024
strlenANSI1039.4568.0460.9353.7306.9254.9212.2145.2 82.0 50.3 35.3 26.4 24.5
strlenBLOG795.4474.9366.4291.0263.9227.6201.7178.3144.5125.9117.2112.0110.5
strlenSSE2703.0340.0267.6196.3167.2131.6108.9 78.7 47.4 30.2 20.4 15.0 14.5
memchrANSI1280.7736.4594.2472.2423.8338.0294.3211.0127.2 90.4 67.3 55.4 55.1
memchrSSE21001.4456.3336.4241.3206.4159.5138.5 93.0 57.5 35.6 23.3 17.0 15.7


コメント (2)

shinh:

一応、 32bit/64bit MacOSX でも動作して、同様に速い感じみたいでした、と報告させてもらいます。

こんにちは。
SSE2を用いたstrlenのコードをRubyにとりこみたいのですが、
ライセンスはどのようになるのでしょうか。
よろしくお願いいたします。

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)