【转】strlen源码

strlen源码剖析

学习高效编程的有效途径之一就是阅读高手写的源代码,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. size_t strlen_c(const char *str) {
  3. const char *char_ptr;
  4. const ulong *longword_ptr;
  5. register ulong longword, magic_bits;
  6. for (char_ptr = str; ((ulong)char_ptr
  7. & (sizeof(ulong) - 1)) != 0;
  8. ++char_ptr) {
  9. if (*char_ptr == '\0')
  10. return char_ptr - str;
  11. }
  12. longword_ptr = (ulong*)char_ptr;
  13. magic_bits = 0x7efefeffL;
  14. while (1) {
  15. longword = *longword_ptr++;
  16. if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0) {
  17. const char *cp = (const char*)(longword_ptr - 1);
  18. if (cp[0] == 0)
  19. return cp - str;
  20. if (cp[1] == 0)
  21. return cp - str + 1;
  22. if (cp[2] == 0)
  23. return cp - str + 2;
  24. if (cp[3] == 0)
  25. return cp - str + 3;
  26. }
  27. }
  28. }

3. 源码剖析
上面给出的C语言实现虽然不算特别复杂,但也值得花点时间来弄清楚,先看9-14行:

  1. for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0; ++char_ptr) {
  2. if (*char_ptr == '\0')
  3. return char_ptr - str;
  4. }

上面的代码实现了数据对齐,如果在对齐之前就遇到'\0'则可以直接return char_ptr - str;

第16行将longword_ptr指向数据对齐后的首地址

  1. longword_ptr = (ulong*)char_ptr;

第18行给magic_bits赋值(在后面会解释这个值的意义)

  1. magic_bits = 0x7efefeffL;

第22行读入一个WORD到longword并将longword_ptr指向下一个WORD

  1. longword = *longword_ptr++;

第24行的if语句是整个算法的核心,该语句判断22行读入的WORD中是否有为0的字节

  1. if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0)

f语句中的计算可以分为如下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:

  1. const char *cp = (const char*)(longword_ptr - 1);
  2. if (cp[0] == 0)
  3. return cp - str;
  4. if (cp[1] == 0)
  5. return cp - str + 1;
  6. if (cp[2] == 0)
  7. return cp - str + 2;
  8. if (cp[3] == 0)
  9. return cp - str + 3;

4. 另一种实现

  1. size_t strlen_d(const char *str) {
  2. const char *char_ptr;
  3. const ulong *longword_ptr;
  4. register ulong longword, himagic, lomagic;
  5. for (char_ptr = str; ((ulong)char_ptr
  6. & (sizeof(ulong) - 1)) != 0;
  7. ++char_ptr) {
  8. if (*char_ptr == '\0')
  9. return char_ptr - str;
  10. }
  11. longword_ptr = (ulong*)char_ptr;
  12. himagic = 0x80808080L;
  13. lomagic = 0x01010101L;
  14. while (1) {
  15. longword = *longword_ptr++;
  16. if (((longword - lomagic) & himagic) != 0) {
  17. const char *cp = (const char*)(longword_ptr - 1);
  18. if (cp[0] == 0)
  19. return cp - str;
  20. if (cp[1] == 0)
  21. return cp - str + 1;
  22. if (cp[2] == 0)
  1. return cp - str + 2;
  2. if (cp[3] == 0)
  3. return cp - str + 3;
  4. }
  5. }

上面的代码与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/spanspan style=------------------------------->0
  longword: 0XXXXXXX0XXXXXXX 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
--------------------------------------------------

转载自:wangjieest的专栏

补充:

现在最新的代码里面if语句的判断条件已经改为:

if (((longword - lomagic) & ~longword& himagic) != 0)

为什么会有&~longword这个呢?看一下下面的解释就知道了:

以4字节整数n为例,我们只要把每个字节分别减去1,如果有纯0的字节存在,必然会有借位,借位之后会在字节最高 位留下一个1。只要判断每个字节的最高位是否存在1就可以了,然而,这里还有一个问题,就是这个4字节整数里,某些字节本来最高 位可能就含有1,所以必须排除掉这些字节。

解决方案:

* 将n的每一个字节分别减1,并取出最高位,得到x,如果存在借位,该字节最高位就是1
  * 将n的每一个字节按位取反并取出最高位,得到y,y中某字节最高位为1,表示它在n里是0
  * 将x和y按位与运算,若不等于0,说明n至少有1字节原本最高位不是1,后来变成1了,就是借位

若n中存在全0字节,则 x&y 一定不为0,因为借位的那个字节最高位会被置为1
若n中不存在全0字节,则不会产生借位,x&y 等于0。
x&y == (n-0x01010101) & ~n & 0x80808080

上一篇:jquery元素查找方法集锦


下一篇:中断 LET′S TRY“嵌入式编程”: 5 of 6