strlen源碼剖析(轉自cppblog)

PS:本文轉自http://www.cppblog.com/ant/archive/2007/10/12/32886.html

學習高效編程的有效途徑之一就是閱讀高手寫的源代碼,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行代碼:

1 size_t strlen_a(const char *str) {
2     size_t length = 0;
3     while (*str++)
4         ++length;
5     return length;
6 }

也許可以稍加改進如下:

1 size_t strlen_b(const char *str) {
2     const char *cp = str;
3     while (*cp++)
4         ;
5     return (cp - str - 1);
6 }

2. 高效實現
很顯然,標準庫的實現肯定不會如此簡單,上面的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個字節):

 1 typedef unsigned long ulong;
 2 
 3 size_t strlen_c(const char *str) {
 4 
 5     const char *char_ptr;
 6     const ulong *longword_ptr;
 7     register ulong longword, magic_bits;
 8 
 9     for (char_ptr = str; ((ulong)char_ptr 
10         & (sizeof(ulong) - 1)) != 0;
11         ++char_ptr) {
12         if (*char_ptr == '\0')
13             return char_ptr - str;
14     }
15 
16     longword_ptr = (ulong*)char_ptr;
17 
18     magic_bits = 0x7efefeffL;
19 
20     while (1) {
21 
22         longword = *longword_ptr++;
23 
24         if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0) {
25 
26             const char *cp = (const char*)(longword_ptr - 1);
27             
28             if (cp[0== 0)
29                 return cp - str;
30             if (cp[1== 0)
31                 return cp - str + 1;
32             if (cp[2== 0)
33                 return cp - str + 2;
34             if (cp[3== 0)
35                 return cp - str + 3;
36         }
37     }
38 }

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. 另一種實現
 1 size_t strlen_d(const char *str) {
 2 
 3     const char *char_ptr;
 4     const ulong *longword_ptr;
 5     register ulong longword, himagic, lomagic;
 6 
 7     for (char_ptr = str; ((ulong)char_ptr 
 8         & (sizeof(ulong) - 1)) != 0;
 9         ++char_ptr) {
10         if (*char_ptr == '\0')
11             return char_ptr - str;
12     }
13 
14     longword_ptr = (ulong*)char_ptr;
15 
16     himagic = 0x80808080L;
17     lomagic = 0x01010101L;
18 
19     while (1) {
20 
21         longword = *longword_ptr++;
22 
23         if (((longword - lomagic) & himagic) != 0) {
24 
25             const char *cp = (const char*)(longword_ptr - 1);
26             
27             if (cp[0== 0)
28                 return cp - str;
29             if (cp[1== 0)
30                 return cp - str + 1;
31             if (cp[2== 0)
32                 return cp - str + 2;
33             if (cp[3== 0)
34                 return cp - str + 3;
35         }
36     }
37 }
上面的代碼與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:  0000000100000001 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。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章