学习高效编程的有效途径之一就是阅读高手写的源代码,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:  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 蚂蚁终结者 阅读(26306) 评论(34)  编辑 收藏 引用 所属分类: The Annotated CRT Sources

Feedback

# re: strlen源码剖析 2007-09-26 19:29 msdn47

果然很快,效率高啊  回复  更多评论

# re: strlen源码剖析 2007-09-26 21:29 hi

(2) ^ ~longword
这一步取出加法后longword中所有未改变的bit。

======================================

这个什么意思?  回复  更多评论

# re: strlen源码剖析 2007-09-27 08:19 蚂蚁终结者

可能我说的不够清楚,看下面的例子:

                   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字节。
 

回复  更多评论

# re: strlen源码剖析 2007-09-27 08:28 wangmuy

赞啊!以前不懂的今天终于看懂了!  回复  更多评论

# re: strlen源码剖析[未登录] 2007-09-27 08:42 漂舟

楼主剖析得全面,顶 !  回复  更多评论

# 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_c比strlen_d要好。因为strlen_c只会漏掉这样的WORD:
    10000000 XXXXXXXX XXXXXXXX XXXXXXXX

http://www.cppblog.com/ant/archive/2007/10/12/32886.aspx

http://www.cnblogs.com/rollenholt/archive/2012/04/12/2443719.html

strlen源码剖析(可查看glibc和VC的CRT源代码)的更多相关文章

  1. strlen源码剖析

      学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简 ...

  2. 【转】strlen源码

    strlen源码剖析 学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码, ...

  3. 自己实现多线程的socket,socketserver源码剖析

    1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...

  4. Node 进阶:express 默认日志组件 morgan 从入门使用到源码剖析

    本文摘录自个人总结<Nodejs学习笔记>,更多章节及更新,请访问 github主页地址.欢迎加群交流,群号 197339705. 章节概览 morgan是express默认的日志中间件, ...

  5. DICOM医学图形处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求(续)

    转载:http://blog.csdn.net/zssureqh/article/details/39237649 背景: 上一篇博文中,在对storescp工具源文件storescp.cc和DcmS ...

  6. DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

    转载:http://blog.csdn.net/zssureqh/article/details/39213817 背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间w ...

  7. Android源码剖析之Framework层升级版(窗口、系统启动)

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 看本篇文章之前,建议先查看: Android源码剖析之Framework层基础版 前面讲了frame ...

  8. 玩转Android之Picasso使用详详详详详详解,从入门到源码剖析!!!!

    Picasso是Squareup公司出的一款图片加载框架,能够解决我们在Android开发中加载图片时遇到的诸多问题,比如OOM,图片错位等,问题主要集中在加载图片列表时,因为单张图片加载谁都会写.如 ...

  9. (升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)

    本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课 ...

随机推荐

  1. PCI的imagework已由freeview软件代替

    作者:朱金灿 来源:http://blog.csdn.net/clever101 在PCI 9.1中重要模块集成显示环境imagework还存在,但是到了PCI 10.0中imagework已经消失了 ...

  2. 开源项目学习之------AG-Admin环境搭建

    原文地址: https://blog.csdn.net/u011781521/article/details/79056974 一.框架简介 AG-Admin是基于Spring Cloud微服务化开发 ...

  3. Springboot系列:@SpringBootApplication注解

    在使用 Springboot 框架进行开发的时候,通常我们会在 main 函数上添加 @SpringBootApplication 注解,今天为大家解析一下 @SpringBootApplicatio ...

  4. Spring boot传统部署

    使用spring boot很方便,一个jar包就可以启动了,因为它里面内嵌了tomcat等服务器. 但是spring boot也提供了部署到独立服务器的方法. 如果你看文档的话,从jar转换为war包 ...

  5. C#委托之个人理解

    C#委托之个人理解   什么是委托 首先要知道什么是委托,用最通俗易懂的话来讲,你就可以把委托看成是用来执行方法(函数)的一个东西. 如何使用委托 在使用委托的时候,你可以像对待一个类一样对待它.即先 ...

  6. mingw qt(可以去掉mingwm10.dll、libgcc_s_dw2-1.dll、libstdc++-6.dll的依赖,mingw默认都是动态链接gcc的库而TDM是静态链接gcc库,tdm版本更好用。用aspack压缩没有问题。qt本身不使用异常处理)good

    原文地址:mingw qt作者:孙1东 不使用Qt SDK,使用mingw编译qt源代码所遇问题及解决方法: configure -fast -release -no-exceptions -no-r ...

  7. Linux核心设计依据(六)该块I/O一层

    块设备是能随机存取装置固定大小的数据表设备.如硬盘:字符设备(如串口和键盘)它是按照字符流进入有序进行.不同之处在于是否足够的随机存取数据--这时候,你可以随心所欲地从一个位置跳到访问设备和位置.复杂 ...

  8. 关于CORS跨域更细节的思考

    权威的资料看MDN,也可以看阮一峰的文章.不过感觉阮一峰对于CORS的描述有问题,简单请求被阮一峰描述为2次浏览器请求了.这个要自己搭个服务器试一下.跨域基本都是根据域名判断的,自己是否要再买个域名呢 ...

  9. 十分钟了解 spring cloud

    1 为什么需要服务发现 简单来说,服务化的核心就是将传统的一站式应用根据业务拆分成一个一个的服务,而微服务在这个基础上要更彻底地去耦合(不再共享DB.KV,去掉重量级ESB),并且强调DevOps和快 ...

  10. windows下安装ffmpeg

    一.下载地址: 网址:https://ffmpeg.org/ 选择Windows版本:https://ffmpeg.org/download.html#build-windows 二.解压安装: 下载 ...