學習高效編程的有效途徑之一就是閱讀高手寫的源代碼,CRT(C/C++ Runtime Library)作為底層的函數庫,實現必然高效。恰好手中就有glibc和VC的CRT源代碼,於是挑了一個相對簡單的函數strlen研究了一下,並對各種實現作了簡單的效率測試。
strlen的函數原形如下:
size_t strlen(const char *str);
strlen返回str中字符的個數,其中str為一個以'\0'結尾的字符串(a null-terminated string)。
1. 簡單實現
如果不管效率,最簡單的實現只需要4行代碼:
也許可以稍加改進如下:
C++ Code 12. 高效實現
很顯然,標准庫的實現肯定不會如此簡單,上面的strlen_a以及strlen_b都是一次判斷一個字符直到發現'\0'為止,這是非常低效的。比較高效的實現如下(在這裡WORD表示計算機中的一個字,不是WORD類型):
(1) 一次判斷一個字符直到內存對齊,如果在內存對齊之前就遇到'\0'則直接return,否則到(2);
(2) 一次讀入並判斷一個WORD,如果此WORD中沒有為0的字節,則繼續下一個WORD,否則到(3);
(3) 到這裡則說明WORD中至少有一個字節為0,剩下的就是找出第一個為0的字節的位置然後return。
NOTE:
數據對齊(data alignment),是指數據所在的內存地址必須是該數據長度的整數倍,這樣CPU的存取速度最快。比如在32位的計算機中,一個WORD為4 byte,則WORD數據的起始地址能被4整除的時候CPU的存取效率比較高。CPU的優化規則大概如下:對於n字節(n = 2,4,8...)的元素,它的首地址能被n整除才能獲得最好的性能。
為了便於下面的討論,這裡假設所用的計算機為32位,即一個WORD為4個字節。下面給出在32位計算機上的C語言實現(假設unsigned long為4個字節):源碼來著於glibc
3. 源碼剖析
上面給出的C語言實現雖然不算特別復雜,但也值得花點時間來弄清楚,先看9-14行:
for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0; ++char_ptr) {
if (*char_ptr == '\0')
return char_ptr - str;
}
上面的代碼實現了數據對齊,如果在對齊之前就遇到'\0'則可以直接return char_ptr - str;
測試代碼如下:
第16行將longword_ptr指向數據對齊後的首地址longword_ptr = (ulong*)char_ptr;
第18行給magic_bits賦值(在後面會解釋這個值的意義)
magic_bits = 0x7efefeffL;
第22行讀入一個WORD到longword並將longword_ptr指向下一個WORD
longword = *longword_ptr++;
第24行的if語句是整個算法的核心,該語句判斷22行讀入的WORD中是否有為0的字節
if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0)
if語句中的計算可以分為如下3步:
(1) longword + magic_bits
其中magic_bits的二進制表示如下:
b3 b2 b1 b0
31------------------------------->0
magic_bits: 01111110 11111110 11111110 11111111
magic_bits中的31,24,16,8這些bits都為0,我們把這幾個bits稱為holes,注意在每個byte的左邊都有一個hole。
檢測0字節:
如果longword 中有一個字節的所有bit都為0,則進行加法後,從這個字節的右邊的字節傳遞來的進位都會落到這個字節的最低位所在的hole上,而從這個字節的最高位則永遠不會產生向左邊字節的hole的進位。則這個字節左邊的hole在進行加法後不會改變,由此可以檢測出0字節;相反,如果longword中所有字節都不為0,則每個字節中至少有1位為1,進行加法後所有的hole都會被改變。
為了便於理解,請看下面的例子:
b3 b2 b1 b0
31------------------------------->0
longword: XXXXXXXX XXXXXXXX 00000000 XXXXXXXX
+ magic_bits: 01111110 11111110 11111110 11111111
上面longword中的b1為0,X可能為0也可能為1。因為b1的所有bit都為0,而從b0傳遞過來的進位只可能是0或1,很顯然b1永遠也不會產生進位,所以加法後longword的第16 bit這個hole不會變。
(2) ^ ~longword
這一步取出加法後longword中所有未改變的bit。
(3) & ~magic_bits
最後取出longword中未改變的hole,如果有任何hole未改變則說明longword中有為0的字節。
根據上面的描述,如果longword中有為0的字節,則if中的表達式結果為非0,否則為0。
NOTE:
如果b3為10000000,則進行加法後第31 bit這個hole不會變,這說明我們無法檢測出b3為10000000的所有WORD。值得慶幸的是用於strlen的字符串都是ASCII標准字符,其值在0-127之間,這意味著每一個字節的第一個bit都為0。因此上面的算法是安全的。
一旦檢測出longword中有為0的字節,後面的代碼只需要找到第一個為0的字節並返回相應的長度就OK:
const char *cp = (const char*)(longword_ptr - 1);
if (cp[0] == 0)
return
cp - str;
if (cp[1] == 0)
return cp - str + 1;
if (cp[2] == 0)
return cp - str + 2;
if (cp[3] == 0)
return cp - str + 3;
4. 另一種實現
CPP Code 1上面的代碼與strlen_c基本一樣,不同的是:
magic_bits換成了himagic和lomagic
himagic = 0x80808080L;
lomagic = 0x01010101L;
以及 if語句變得比較簡單了
if (((longword - lomagic) & himagic) != 0)
if語句中的計算可以分為如下2步:
(1) longword - lomagic
himagic和lomagic的二進制表示如下:
b3 b2 b1 b0
31------------------------------->0
himagic: 10000000 10000000 10000000 10000000
lomagic: 00000001 00000001 00000001 00000001
在這種方法中假設所有字符都是ASCII標准字符,其值在0-127之間,因此longword總是如下形式:
b3 b2 b1 b0
31------------------------------->0
longword: 0XXXXXXX 0XXXXXXX 0XXXXXXX 0XXXXXXX
檢測0字節:
如果longword 中有一個字節的所有bit都為0,則進行減法後,這個字節的最高位一定會從0變為1;相反,如果longword中所有字節都不為0,則每個字節中至少有1位為1,進行減法後這個字節的最高位依然為0。
(2) & himagic
這一步取出每個字節最高位的1,如果有任意字節最高位為1則說明longword中有為0的字節。
根據上面的描述,如果longword中有為0的字節,則if中的表達式結果為非0,否則為0。
5. 匯編實現
VC CRT的匯編實現與前面strlen_c算法類似
6. 測試結果
為了對上述各種實現的效率有一個大概的認識,我在VC8和GCC下分別進行了測試,測試時均采用默認優化方式。下面是在GCC下運行幾百萬次後的結果(在VC8下的運行結果與此相似):
strlen_a
--------------------------------------------------
1: 515 ticks 0.515 seconds
2: 375 ticks 0.375 seconds
3: 375 ticks 0.375 seconds
4: 375 ticks 0.375 seconds
5: 375 ticks 0.375 seconds
total: 2015 ticks 2.015 seconds
average: 403 ticks 0.403 seconds
--------------------------------------------------
strlen_b
--------------------------------------------------
1: 360 ticks 0.36 seconds
2: 390 ticks 0.39 seconds
3: 375 ticks 0.375 seconds
4: 360 ticks 0.36 seconds
5: 375 ticks 0.375 seconds
total: 1860 ticks 1.86 seconds
average: 372 ticks 0.372 seconds
--------------------------------------------------
strlen_c
--------------------------------------------------
1: 187 ticks 0.187 seconds
2: 172 ticks 0.172 seconds
3: 187 ticks 0.187 seconds
4: 187 ticks 0.187 seconds
5: 188 ticks 0.188 seconds
total: 921 ticks 0.921 seconds
average: 184 ticks 0.1842 seconds
--------------------------------------------------
strlen_d
--------------------------------------------------
1: 172 ticks 0.172 seconds
2: 187 ticks 0.187 seconds
3: 172 ticks 0.172 seconds
4: 187 ticks 0.187 seconds
5: 188 ticks 0.188 seconds
total: 906 ticks 0.906 seconds
average: 181 ticks 0.1812 seconds
--------------------------------------------------
strlen
--------------------------------------------------
1: 187 ticks 0.187 seconds
2: 172 ticks 0.172 seconds
3: 188 ticks 0.188 seconds
4: 172 ticks 0.172 seconds
5: 187 ticks 0.187 seconds
total: 906 ticks 0.906 seconds
average: 181 ticks 0.1812 seconds
--------------------------------------------------