代码改变世界

[转载]strlen源码剖析

2012-02-22 09:48  会被淹死的鱼  阅读(417)  评论(0编辑  收藏  举报

原文出处: http://www.cppblog.com/ant/archive/2007/10/12/32886.html

这篇#ant的博文对strlen分析的很详细, 很是受用, glibc新版的实现不是文中的版本d.

 1 size_t
2 strlen (str)
3 const char *str;
4 {
5 const char *char_ptr;
6 const unsigned long int *longword_ptr;
7 unsigned long int longword, himagic, lomagic;
8
9 /* Handle the first few characters by reading one character at a time.
10 Do this until CHAR_PTR is aligned on a longword boundary. */
11 for (char_ptr = str; ((unsigned long int) char_ptr
12 & (sizeof (longword) - 1)) != 0;
13 ++char_ptr)
14 if (*char_ptr == '\0')
15 return char_ptr - str;
16
17 /* All these elucidatory comments refer to 4-byte longwords,
18 but the theory applies equally well to 8-byte longwords. */
19
20 longword_ptr = (unsigned long int *) char_ptr;
21
22 /* Bits 31, 24, 16, and 8 of this number are zero. Call these bits
23 the "holes." Note that there is a hole just to the left of
24 each byte, with an extra at the end:
25
26 bits: 01111110 11111110 11111110 11111111
27 bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD
28
29 The 1-bits make sure that carries propagate to the next 0-bit.
30 The 0-bits provide holes for carries to fall into. */
31 himagic = 0x80808080L;
32 lomagic = 0x01010101L;
33 if (sizeof (longword) > 4)
34 {
35 /* 64-bit version of the magic. */
36 /* Do the shift in two steps to avoid a warning if long has 32 bits. */
37 himagic = ((himagic << 16) << 16) | himagic;
38 lomagic = ((lomagic << 16) << 16) | lomagic;
39 }
40 if (sizeof (longword) > 8)
41 abort ();
42
43 /* Instead of the traditional loop which tests each character,
44 we will test a longword at a time. The tricky part is testing
45 if *any of the four* bytes in the longword in question are zero. */
46 for (;;)
47 {
48 longword = *longword_ptr++;
49
50 if (((longword - lomagic) & ~longword & himagic) != 0)
51 {
52 /* Which of the bytes was the zero? If none of them were, it was
53 a misfire; continue the search. */
54
55 const char *cp = (const char *) (longword_ptr - 1);
56
57 if (cp[0] == 0)
58 return cp - str;
59 if (cp[1] == 0)
60 return cp - str + 1;
61 if (cp[2] == 0)
62 return cp - str + 2;
63 if (cp[3] == 0)
64 return cp - str + 3;
65 if (sizeof (longword) > 4)
66 {
67 if (cp[4] == 0)
68 return cp - str + 4;
69 if (cp[5] == 0)
70 return cp - str + 5;
71 if (cp[6] == 0)
72 return cp - str + 6;
73 if (cp[7] == 0)
74 return cp - str + 7;
75 }
76 }
77 }
78 }

其中22-30的行的注释已经过时了, 就是文中版本c的注释.

判断条件if (((longword - lomagic) & ~longword & himagic) != 0)有了一些新的变化, 原理还是类似的.

longword - lomagic 如果longword中有某一个字节都为0, 就会导致最高位出现1, 而本身ACSII中的最高位是0.

(longword - lomagic) & ~longword 取出字节中原本为0的位

(longword - lomagic) & ~longword & himagic 最后判断最高位是否变化, 有变化说明有字节为0, 然后逐个判断是否为0, 计算长度.

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

学习高效编程的有效途径之一就是阅读高手写的源代码,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。

                   b3      b2       b1       b0
               31------------------------------->0
     longword: 00001001 00011000 00000000 00001100
+  magic_bits: 01111110 11111110 11111110 11111111
          sum: 10001000 00010110 11111111 00001011
^   ~longword: 11110110 11100111 11111111 11110011
            a: 01111110 11110001 00000000 11111000
& ~magic_bits: 10000001 00000001 00000001 00000000
       result: 00000000 00000001 00000000 00000000

sum = longword + magic_bits;
a = sum ^ ~longword;
即用sum与longword逐位比较,如果有某个位相同,就说这个位在加法后未改变,这样在a中为1的位就是未改变的。

 
result = a & ~magic_bits;
得到未改变的hole位,从上例可以看出第16 bit这个hole加法后未改变,这样就检测出了0字节。



(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:  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    ( 010000 )
 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
--------------------------------------------------


源代码:点击下载