RapidJSON中itoa的實現是現在已知最快的么?

miloyip/itoa-benchmark · GitHub


哈哈這個我要來摻和一下。 @Milo Yip剛放出itoa benchmark的時候我也設計了一個比較奇葩的演算法,而且經過我人肉micro-op分析,理應比milo的sse2版本要快。雖然實際運行結果證明我的演算法並不快(所以這也說明人肉性能分析實在不靠譜),但是我覺得還是可以分享一下。

鑒於sse2版本的itoa已經足夠優秀,要想打敗它,基本上只有一種選擇:完全避開周期較長的乘法指令。這怎麼能夠做到呢,我的做法是,打一張巨大的表。比如若要將0x12345678這個數轉成10進位表示,只需要預先計算出0x12000000, 0x340000, 0x5600, 0x78這四個數字的十進位表示,然後加起來即可,這個加法可以藉助於sse一條指令搞定。注意這裡還有一個問題是需要按10進位傳播進位,但sse是按256進位的,一個小技巧是在每一個lane上額外加上256 - 10 = 246,這樣就可以用sse模擬10進位加法了。完整代碼如下:

__m128i Convert8DigitsSSE4Lut(uint32_t value)
{
assert(value &<= 99999999); __m128i a = _mm_cvtsi64_si128(gSSEDigitsLUT3[value &>&> 24]);
__m128i b = _mm_cvtsi64_si128(gSSEDigitsLUT2[(value &>&> 16) 0xFF]);
__m128i c = _mm_cvtsi64_si128(gSSEDigitsLUT1[(value &>&> 8) 0xFF]);
__m128i d = _mm_cvtsi32_si128(gSSEDigitsLUT0[value 0xFF]);

__m128i k = _mm_cvtsi64_si128(kBroadcast);
__m128i z = _mm_cvtsi64_si128(kAscIIzero);
__m128i r = _mm_cvtsi64_si128(kRevertMask);

__m128i x = _mm_add_epi64(a, b);
__m128i y = _mm_add_epi64(c, d);
x = _mm_max_epu8(x, _mm_add_epi8(x, k));
y = _mm_min_epu8(y, _mm_sub_epi8(y, k));
x = _mm_add_epi64(x, y);
x = _mm_min_epu8(x, _mm_sub_epi8(x, k));

x = _mm_shuffle_epi8(x, r);
x = _mm_add_epi8(x, z);

return x;
}

注意上面的每一條指令都只需要1個cycle(LOAD除外),所有的加起來也不過相當於一條乘法指令的開銷。而進一步跑了一下IACA也基本印證了我的人肉分析:

我的sse4lut: 12 cycles的latency

Latency Analysis Report

---------------------------

Latency: 12 Cycles

| Inst | Resource Delay In Cycles | |

| Num | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | FE | |

-------------------------------------------------------------------------

| 0 | | | | | | | | | | | lea r8, ptr [rip-0x1252]

| 1 | | | | | | | | | | | mov rax, 0xf6f6f6f6f6f6f6f6

| 2 | | | | | | | | | | | movq xmm4, rax

| 3 | | | | | | | | | | | mov eax, ecx

| 4 | | | | | | | | | | | shr rax, 0x10

| 5 | | | | | | | | | | | movzx ecx, al

| 6 | | | | | | | | | 1 | | mov eax, edx

| 7 | | | | | | | 1 | | | | shr rax, 0x18

| 8 | | | | | | | | | | | movq xmm2, qword ptr [r8+rcx*8+0x3b80]

| 9 | | | | | | | | | | | movq xmm0, qword ptr [r8+rax*8+0x3380]

| 10 | | | | | | | | | 2 | CP | mov eax, edx

| 11 | 1 | | | | | | | | | CP | shr rax, 0x8

| 12 | | | | | | | | | | CP | movzx ecx, al

| 13 | | | | | | | | | 3 | | movzx eax, dl

| 14 | | | | | | | | | | | paddq xmm2, xmm0

| 15 | | | | | | | | | | | movd xmm0, dword ptr [r8+rax*4+0x4b80]

| 16 | | | | | | | | | | CP | movq xmm3, qword ptr [r8+rcx*8+0x4380]

| 17 | | | | | | | | | 4 | | mov rax, 0x1020304050607

Milo的sse2 : 43 cycles的latency

Latency Analysis Report

---------------------------

Latency: 43 Cycles

| Inst | Resource Delay In Cycles | |

| Num | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | FE | |

-------------------------------------------------------------------------

| 0 | | | | | | | | | | CP | movdqa xmm2, xmmword ptr [rip+0x1f6f]

| 1 | | | | | | | | | | | movdqa xmm0, xmmword ptr [rip+0x1f57]

| 2 | | | | | | | | | | | movd xmm1, ebx

| 3 | | | | | | | | | | | inc ebx

| 4 | | | | | | | | | | CP | pmuludq xmm2, xmm1

| 5 | | | | | | | | | | CP | psrlq xmm2, 0x2d

| 6 | | | | | | | | | | CP | pmuludq xmm0, xmm2

| 7 | | | | | | | | | | CP | psubd xmm1, xmm0

| 8 | | | | | | | | | | CP | punpcklwd xmm2, xmm1

| 9 | | | | | | | | | | CP | psllq xmm2, 0x2

| 10 | | | | | | | | | | CP | punpcklwd xmm2, xmm2

| 11 | | | | | | | | | | CP | punpckldq xmm2, xmm2

| 12 | | | | | | | | | 3 | CP | pmulhuw xmm2, xmmword ptr [rip+0x1f17]

| 13 | | | | | | | | | 3 | CP | pmulhuw xmm2, xmmword ptr [rip+0x1eff]

| 14 | | | | | | | | | | CP | movdqa xmm0, xmm2

| 15 | | | 1 | | | | | | 3 | CP | pmullw xmm0, xmmword ptr [rip+0x1ee3]

| 16 | | | | | | | | | | CP | psllq xmm0, 0x10

| 17 | | | | | | | | | | CP | psubw xmm2, xmm0

| 18 | | | | | | | | | | CP | packuswb xmm2, xmm3

| 19 | | | | | | | | | 4 | CP | paddb xmm2, xmmword ptr [rip+0x1ebe]

| 20 | | | | | | | | | 5 | CP | movq qword ptr [rsp+0x20], xmm2

| 21 | | | | | | | | | 4 | | cmp ebx, 0x1f82088

| 22 | | | | | | | | | | | jb 0xffffffffffffff82

| 23 | | | | | | | | | 5 | | lea rcx, ptr [rsp+0x80]

看上去非常有希望是不是,12 cycles VS 43 cycles, 簡直完爆啊,可以實際運行結果確是這樣的(我的測試代碼已經儘可能的讓LUT全部L1命中):

sse4lut: 89.7469

sse2: 70.7487

媽蛋以後再不相信Intel的手冊了。


很難說。benchmark只是特定平台、編譯器下的結果,有可能在某些情況下不是最優,例如或可用到某些CPU提供的指令集。

SSE2版本只在多數位的情況下比branchlut快一點點,所以RapidJSON只使用了後者。

如果發現有更好的實現,可簡單地加入這個benchmark中,並希望能給pull request。


看起來是


推薦閱讀:

如何寫好一個parser?
如何看待25歲博士雷霄驊猝死?
如何打敗超前學習的人?
搞計算機的要考證么?
列車運行圖編製的程序和方法是是什麼?

TAG:演算法 | 編程 | C | RapidJSON |