目前已經有多種將整形資料轉換為字串表示式的方法。雖然這些轉換方法很少會遇到什麼瓶頸,但是在分析特定應用的時候就有可能了。比如,在Lwan裡面構建響應頭部的時候就經常會出現。
就拿Lwan來說吧,最初是用snprintf()函式來轉換數字。雖然在表面上這確實能起作用,但是卻太沒勁了。
第二種方法是使用樸素演算法:將原數連續與10相除,每次都把模轉換成一個字元加在字串後,當除到最後的餘數為0時就停止並將字串倒序得到最後的字串。
1 2 3 4 5 6 7 8 9 10 11 12 |
// Code based on https://code.google.com/p/stringencoders/ size_t naive_uint32_to_str(uint32_t value, char *str) { char *wstr = str; // Conversion. Number is reversed. do *wstr++ = (char) decimal_digits[uvalue % 10]; while (uvalue /= 10); *wstr = ''; // Reverse string strreverse(str, wstr - 1); return wstr - str; } |
這在一般情況下還是可以的,但倒轉字串的那一步總是令我困擾,為什麼不直接向後寫字串呢?
之後我就把Lawn的程式碼改寫成了如下程式碼段。需要注意的是,無論sizeof(int32_t)是多少,我都把數字的最大的位元組大小(包括終止符)設定成了MAX_INT的3倍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define INT_TO_STR_BUFFER_SIZE (3 * sizeof(int32_t)) char *lwan_uint32_to_str(uint32_t value, char buffer[static INT_TO_STR_BUFFER_SIZE], size_t *len) { char *p = buffer + INT_TO_STR_BUFFER_SIZE - 1; *p = ''; do { *--p = "0123456789"[value % 10]; } while (value /= 10); size_t difference = (size_t)(p - buffer); *len = (size_t)(INT_TO_STR_BUFFER_SIZE - difference - 1; return p; } |
減少陣列的寫入操作使得演算法速度明顯加快。然而,在我修補剛才那個演算法的時候我卻犯了一個很多人都會盡量避免的錯誤:我讓陣列進行了額外的查詢工作,在沒有測試它的表現是否會更好的情況下就不管三七二十一提交了程式碼。如果使用查表法會比這快9%,噢!
就在去年,Facebook的工程團隊釋出了一個更快的將整數轉換成字串的函式。他們同樣避免了將各個數字轉換後形成的字串轉置的操作,並且他們把查表法運用得很好。
這裡的技巧就是,他們把這張表做成了從00到99的數值對,而不是簡單的10個數字。這樣就把除法運算的數量減少了一半,演算法的效能得到很大的提升:比上面的程式碼段快了大概31%:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
size_t facebook_uint32_to_str(uint32_t value, char *dst) { static const char digits[201] = "0001020304050607080910111213141516171819" "2021222324252627282930313233343536373839" "4041424344454647484950515253545556575859" "6061626364656667686970717273747576777879" "8081828384858687888990919293949596979899"; size_t const length = digits10(value); size_t next = length - 1; while (value >= 100) { auto const i = (value % 100) * 2; value /= 100; dst[next] = digits[i + 1]; dst[next - 1] = digits[i]; next -= 2; } // Handle last 1-2 digits if (value < 10) { dst[next] = '0' + uint32_t(value); } else { auto i = uint32_t(value) * 2; dst[next] = digits[i + 1]; dst[next - 1] = digits[i]; } return length; } |
digits10()函式是另外一個使用特殊方式計算數字裡面數字個數的函式。即使是高效能,我們也得想法防止一起呼叫這些東西:使用一個像numeric_limits<uint32_t>::digits10的常量來保持介面的一致性。這是可以實現的,因為dst快取應該有足夠的大小去容納最大32位的無符號整型資料。
這個函式基本上都是在把數字和10的次方相比較,並且當數字的位數超過了他們要比較的數的最大次方時就遞迴。由於這種實現細節,對於一個很小的數使用一個不變的長度並不會使速度得到顯著的提升(比如一位或兩位數字);但如果你是出於優化的角度講,那麼使用一個常量並無大礙。如此,在我的機器上(一款搭載酷睿i7 2640M裝有最新64位Arch Linux系統的筆記本),它始終都會執行得更快:
上面這張圖來源於我自己寫的一個能夠測試上面所有的整型轉字串方法的一個標準的程式。下面是一更完整的表,裡面還和其它的一些方法進行了對比。
不幸的是,本文存在這一個許可問題,它並不允許我使用Lawn的程式碼。這篇部落格文章並沒有提到這個許可。我是在《two-digit lookup table in places unrelated to Facebook》發現這個演算法的,所以我並不確定到底是誰最先提出的。上面這些問題的很大的一個來源是Hacker’s Delight網站,但是現在在那裡卻找不到了。