strlen講解和快速確定字符串結束符的位置

 


 

strlen源碼剖析快速確定字符串結束符位置

整理分別來自於下面的文章。

http://code.google.com/p/strstrsse/source/browse/trunk/

http://www.cppblog.com/djxzh/archive/2008/10/27/65245.aspx

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位的計算機中,一個WORD4 byte則WORD數據的起始地址能被4整除的時候CPU的存取效率比較高。CPU的優化規則大概如下:對於n字節(n = 2,4,8...)的元素,它的首地址能被n整除才能獲得最好的性能。

              爲了便於下面的討論,這裏假設所用的計算機爲32位,即一個WORD爲4個字節。下面給出在32位計算機上的C語言實現(假設unsigned long爲4個字節):

 

 1 typedef unsigned long ulong;

 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;

    /*實現了數據對齊,如果在對齊之前就遇到'\0'則可以直接return char_ptr - str;*/

 9     for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0; ++char_ptr) {
12         if (*char_ptr == '\0')
13             return char_ptr - str;
14      }

16     longword_ptr = (ulong*)char_ptr; /*指向數據對齊後的首地址*/
17      magic_bits 0x7efefeffL;
[S1] 20     while (1) {
22          longword = *longword_ptr++;
24          if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0) {
25              const char *cp = (const char*)(longword_ptr - 1);
27              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
(2)檢測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 1111111
0 11111110 11111111

上面longword中的b1爲0,X可能爲0也可能爲1。因爲b1的所有bit都爲0,而從b0傳遞過來的進位只可能是0或1,很顯然b1永遠也不會產生進位,所以加法後longword的第16 bit這個hole不會變。

(2)  ^ ~longword
這一步取出加法後longword中所有未改變的bit
使特定位的值取反 (mask中特定位置1,其它位爲0 s=s^mask)

(3)  & ~magic_bits
最後取出longword中未改變的hole,如果有任何hole未改變則說明longword中有爲0的字節
按位與運算:取某數中指定位 (mask中特定位置1,其它位爲0s=s&mask)

根據上面的描述,如果longword中有爲0的字節,則if中的表達式結果爲非0,否則爲0
NOTE
如果b310000000,則進行加法後第31 bit這個hole不會變,這說明我們無法檢測出b310000000的所有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:  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算法類似

  1         page    ,132
  2         title   strlen - return the length of a null-terminated string
  3 ;***
  4 ;strlen.asm - contains strlen() routine
  5 ;
  6 ;       Copyright (c) Microsoft Corporation. All rights reserved.
  7 ;
  8 ;Purpose:
  9 ;       strlen returns the length of a null-terminated string,
 10 ;       not including the null byte itself.
 11 ;
 12 ;*******************************************************************************
 13 
 14         .xlist
 15         include cruntime.inc
 16         .list
 17 
 18 page
 19 ;***
 20 ;strlen - return the length of a null-terminated string
 21 ;
 22 ;Purpose:
 23 ;       Finds the length in bytes of the given string, not including
 24 ;       the final null character.
 25 ;
 26 ;       Algorithm:
 27 ;       int strlen (const char * str)
 28 ;       {
 29 ;           int length = 0;
 30 ;
 31 ;           while( *str++ )
 32 ;                   ++length;
 33 ;
 34 ;           return( length );
 35 ;       }
 36 ;
 37 ;Entry:
 38 ;       const char * str - string whose length is to be computed
 39 ;
 40 ;Exit:
 41 ;       EAX = length of the string "str", exclusive of the final null byte
 42 ;
 43 ;Uses:
 44 ;       EAX, ECX, EDX
 45 ;
 46 ;Exceptions:
 47 ;
 48 ;*******************************************************************************
 49 
 50         CODESEG
 51 
 52         public  strlen
 53 
 54 strlen  proc \
 55         buf:ptr byte
 56 
 57         OPTION PROLOGUE:NONE, EPILOGUE:NONE
 58 
 59         .FPO    ( 0, 1, 0, 0, 0, 0 )
 60 
 61 string  equ     [esp + 4]
 62 
 63         mov     ecx,string              ; ecx -> string
 64         test    ecx,3                   ; test if string is aligned on 32 bits
 65         je      short main_loop
 66 
 67 str_misaligned:
 68         ; simple byte loop until string is aligned
 69         mov     al,byte ptr [ecx]
 70         add     ecx,1
 71         test    al,al
 72         je      short byte_3
 73         test    ecx,3
 74         jne     short str_misaligned
 75 
 76         add     eax,dword ptr 0         ; 5 byte nop to align label below
 77 
 78         align   16                      ; should be redundant
 79 
 80 main_loop:
 81         mov     eax,dword ptr [ecx]     ; read 4 bytes
 82         mov     edx,7efefeffh
 83         add     edx,eax
 84         xor     eax,-1
 85         xor     eax,edx
 86         add     ecx,4
 87         test    eax,81010100h
 88         je      short main_loop
 89         ; found zero byte in the loop
 90         mov     eax,[ecx - 4]
 91         test    al,al                   ; is it byte 0
 92         je      short byte_0
 93         test    ah,ah                   ; is it byte 1
 94         je      short byte_1
 95         test    eax,00ff0000h           ; is it byte 2
 96         je      short byte_2
 97         test    eax,0ff000000h          ; is it byte 3
 98         je      short byte_3
 99         jmp     short main_loop         ; taken if bits 24-30 are clear and bit
100                                         ; 31 is set
101 
102 byte_3:
103         lea     eax,[ecx - 1]
104         mov     ecx,string
105         sub     eax,ecx
106         ret
107 byte_2:
108         lea     eax,[ecx - 2]
109         mov     ecx,string
110         sub     eax,ecx
111         ret
112 byte_1:
113         lea     eax,[ecx - 3]
114         mov     ecx,string
115         sub     eax,ecx
116         ret
117 byte_0:
118         lea     eax,[ecx - 4]
119         mov     ecx,string
120         sub     eax,ecx
121         ret
122 
123 strlen  endp
124 
125         end


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
--------------------------------------------------

posted on 2007-10-12 13:19螞蟻終結者閱讀(14436)評論(34)  編輯 收藏引用所屬分類:The Annotated CRT Sources

(2) ^ ~longword
這一步取出加法後longword中所有未改變的bit

======================================
這個什麼意思?  回覆  更多評論  

# re: strlen源碼剖析 2007-09-27 08:19 螞蟻終結者

可能我說的不夠清楚,看下面的例子:

                   b3      b2       b1       b0
               31------------------------------->0
     longword: 0000
1001 00011000 00000000 00001100
+  magic_bits: 01111110 1111111
0 11111110 11111111

          sum: 10001000 0001011011111111 00001011

^   ~longword: 11110110 1110011111111111 11110011

            a: 01111110 1111000100000000 11111000

& ~magic_bits: 10000001 00000001 00000001 00000000

       result: 00000000 0000000100000000 00000000

sum = longword + magic_bits;
a = sum ^ ~longword;
即用sumlongword逐位比較,如果有某個位相同,就說這個位在加法後未改變這樣在a中爲1的位就是未改變的。

 

result = a & ~magic_bits;
得到未改變的hole,從上例可以看出第16 bit這個hole加法後未改變,這樣就檢測出了0字節。

 # re: strlen源碼剖析 2007-09-27 08:52 Yong Sun

只能檢測0~127,是個問題,最好能兼容0~255,有部分製表符和擴展字符位於這個區域。  回覆  更多評論  

# re: strlen源碼剖析 2007-09-27 09:06 螞蟻終結者

實際上0~255都能檢測出來的:

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;

如果上面的語句執行完還沒有return,則會繼續下一次循環,這樣還是能檢測到在if語句中漏掉的128~255,只不過效率上會有所損失。如果要檢測0~255之間的字符,strlen_cstrlen_d要好。因爲strlen_c只會漏掉這樣的WORD

    10000000 XXXXXXXX XXXXXXXX XXXXXXXX

  回覆  更多評論  

# re: strlen源碼剖析 2007-09-27 09:23 k120

爲了便於下面的討論,這裏假設所用的計算機爲32位,即一個WORD4個字節?呵呵,筆誤吧?
  回覆
  更多評論  

# re: strlen源碼剖析 2007-09-27 09:35 螞蟻終結者

Sorry,我不該用WORD這個單詞。我說的WORD在這裏表示計算機中的一個字長,不是一般爲2個字節的WORD類型。
  回覆
  更多評論  

# re: strlen源碼剖析 2007-09-27 10:32 Yong Sun

另外,是否有endian的問題呢?  回覆  更多評論  

# re: strlen源碼剖析 2007-09-27 10:39 Yong Sun

想了想,應該沒有,呵呵  回覆  更多評論  

# re: strlen源碼剖析 2007-09-27 11:16 螞蟻終結者

c語言的版本不會有endian的問題,如果用匯編就需要注意了。
假設有4個連續的字節abcd,現在要找出其中的第一個0字節:
1.
PowerPC這種big-endian的計算機上,將這4個字節讀到寄存器中依然是
abcd
,從左到右找到最左邊的0字節就OK了。
2.
X86這種little-endian的計算機上,將這4個字節讀到寄存器中就會變成
dcba
,這就需要從右到左找到最右邊的0字節。
可以看出,上面VC的彙編實現是針對X86計算機的。
  回覆
  更多評論  

# re: strlen源碼剖析 2007-09-27 12:51 ∑x

如果能做到這樣的分析,還有什麼能學不會?!  回覆  更多評論  

# re: strlen源碼剖析 2007-10-12 16:36 Minidx全文檢索

幾天前看過的文章怎麼又跑前面來了?!  回覆  更多評論  

# re: strlen源碼剖析 2007-11-02 13:04 really green

for (char_ptr = str; ((ulong)char_ptr
& (sizeof(ulong) - 1)) != 0 ;
++ char_ptr) {
if (*char_ptr == '\0' )
return char_ptr - str;
}
我比較菜,這裏就看不明白:
((ulong)char_ptr& (sizeof(ulong) - 1)) != 0 ;
(ulong)char_ptr 這個轉換把一個 char *轉成 ulong會發生什麼事情?  回覆  更多評論  

# re: strlen源碼剖析 2007-11-02 19:28 really green

想了想這個問題問得挺幼稚,我理解 (ulong)char_ptr應該得到一個ulong的值,這個值就是指針char_ptr的值。 

 回覆  更多評論  

# re: strlen源碼剖析 2007-11-04 09:35 螞蟻終結者

you gotta it!  回覆  更多評論  

# re: strlen源碼剖析 2007-12-04 05:39 福福

如果有中文字符,這個是不是有問題
if (((longword - lomagic) & himagic) != 0)  回覆
  更多評論  

# re: strlen源碼剖析 2007-12-05 19:39 螞蟻終結者

中文字符應該用wcslen纔對,strlen是用來處理單字節字符串的。
詳細描述請看MSDN
However, strlen interprets the string as a single-byte character string, so its return value is always equal to the number of bytes, even if the string contains multibyte characters. wcslen is a wide-character version of strlen; the argument of wcslen is a wide-character string and the count of characters is in wide (two-byte) characters.
  回覆
  更多評論  

# re: strlen源碼剖析 2007-12-27 14:01 Fox

幾年沒動過彙編了,看了代碼,有種耳目一新的感覺~~~~~~~~  回覆  更多評論  

# re: strlen源碼剖析 2007-12-31 17:44 Sunky

感謝博主的精彩剖析。
感覺到我不會的東西太多了
由此堅定了我看GNU C的決心
謝謝  回覆  更多評論  

# re: strlen源碼剖析 2008-09-07 16:11 star

博主的文章允許轉載麼?
昨天看glibc裏的註釋覺得雲裏霧裏
今天看了博主的文章,突然覺得豁然開朗啊 ;-)
還有想問一個問題 glibc裏是這樣處理64位的
if (sizeof (longword) > 4)
{
/* 64-bit version of the magic. */
/* Do the shift in two steps to avoid a warning if long has 32 bits. */
magic_bits = ((0x7efefefeL << 16) << 16) | 0xfefefeffL;
himagic = ((himagic << 16) << 16) | himagic;
lomagic = ((lomagic << 16) << 16) | lomagic;

}
爲什麼不一次就移32位呢?  回覆  更多評論  

# re: strlen源碼剖析 2008-09-07 19:54 螞蟻終結者

@star
歡迎轉載,轉載當然是沒有問題的,畢竟寫文章就是能讓更多的人看到!

爲什麼不一次就移32位呢?
我也不太清楚,可能就其中註釋所說:
/* Do the shift in two steps to avoid a warning if long has 32 bits. */
只是爲了避免warn吧呵呵!  回覆  更多評論  

# re: strlen源碼剖析 2008-11-07 11:42 test

似乎CRT庫的這種寫法有點越界訪問的意思,呵呵。  回覆  更多評論  

# re: strlen源碼剖析 2009-03-18 17:08 123

@福福
strlen
算法在處理中文時不會出問題,關鍵點在後面的if(cp[?]==0)
不過處理中文時效率會跟最簡單的strlen一樣。  回覆  更多評論  

# re: strlen源碼剖析 2009-04-29 14:32 uestc

分析得很清楚,寫得也很詳細,贊一個。  回覆  更多評論  

# re: strlen源碼剖析 2009-06-05 14:06 宋兵乙

for (char_ptr = str; ((ulong)char_ptr
& (sizeof(ulong) - 1)) != 0 ;
++ char_ptr) {
if (*char_ptr == '\0' )
return char_ptr - str;
}
如上的代碼我看不懂了,我的分析如下,麻煩各位高手指出錯誤。
sizeof(ulong)-1
等於3,也就是二進制的0000 0011,於是想要跳出for循環,只需char_ptr的最後兩位是0即可。而每次for循環時,char_ptr執行了++操作,由於char_ptr是指向char的指針,因此每次++應該是增加一個char的長度就是8。表現在二進制上就是倒數第4位增加1,而後兩位是不會變化的。故得出結論若char_ptr的初始值不滿足最後兩位是0,那for就永遠是死循環了。
求各位指出上述論證錯誤在哪。另外,我沒明白此例中內存對齊到滿足哪種條件才能提高代碼效率。  回覆  更多評論  

# re: strlen源碼剖析 2009-06-08 11:51 螞蟻終結者

@宋兵乙
由於char_ptr是指向char的指針,因此每次++應該是增加一個char的長度就是8”
看來你對指針運算還不太瞭解,建議好好複習一下指針部分。
授人魚不如授人漁呵呵  回覆  更多評論  

# re: strlen源碼剖析 2009-10-29 11:31 似水之心

學習,謝謝分享  回覆  更多評論  

# re: strlen源碼剖析 2010-07-31 20:38 hoodlum1980

不錯。我今天反彙編看到strlen的彙編代碼一直覺得很奇怪,
沒搞懂這幾句話在幹什麼。。。看了這篇文章很有幫助。

 


 [S1] 01111110 11111110 11111110 11111111

 

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