メイン

SIMD アーカイブ

2008年06月07日

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アライメントされたところを超えることはないのでバグで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(p);
    size_t n = ip & 15;
    if (n > 0) {
        ip &= ~15;
        __m128i x = *(const __m128i*)ip;
        __m128i a = _mm_cmpeq_epi8(x, c16);
        unsigned long mask = _mm_movemask_epi8(a);
        mask &= 0xffffffffUL << n;
        if (mask) {
            return bsf(mask) - n;
        }
        p += 16 - n;
    }
    /*
        thanks to egtra-san
    */
    assert((reinterpret_cast(p) & 15) == 0);
    if (reinterpret_cast(p) & 31) {
        __m128i x = *(const __m128i*)&p[0];
        __m128i a = _mm_cmpeq_epi8(x, c16);
        unsigned long mask = _mm_movemask_epi8(a);
        if (mask) {
            return p + bsf(mask) - top;
        }
        p += 16;
    }
    assert((reinterpret_cast(p) & 31) == 0);
    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 long 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

追記(2008/6/13)
64bit Windowsで_BitScanForwardの引数の型を勘違いしていたので修正.
ソースにライセンスを修正BSDライセンスと明記.

追記(2008/7/16)
strlenSSE2でループアンロールのため32byte単位で読み込んでいたため,ページングの境界を超えてしまうことがあったバグを修正(thanks to egtraさん).

About SIMD

ブログ「mitsunari@cybozu labs」のカテゴリ「SIMD」に投稿されたすべてのエントリーのアーカイブのページです。過去のものから新しいものへ順番に並んでいます。

前のカテゴリはSHA-1です。

次のカテゴリはboostです。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。