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

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

也许可以稍加改进如下:

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

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个字节):源码来着于glibc

C++ Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typedef unsigned long  ulong;

size_t strlen_c(const char *str)
{

    const char *char_ptr;
    const ulong *longword_ptr;
    register ulong longword, magic_bits;

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

    longword_ptr = (ulong * )char_ptr;

    magic_bits = 0x7efefeffL ;

    while (1 )
    {

        longword = *longword_ptr++ ;

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

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

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;
测试代码如下:

ASM Code
1
2
3
4
char szName[]=”Jack”;
char *p=szName;
p++;//p移动一个字节,本身的地址p是按照4个字节对齐的,移动后不再对齐
printf(“%d\n”,strlen(p));

第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. 另一种实现

CPP Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

size_t strlen_d(const char * str)

{

    const char * char_ptr;

    const ulong * longword_ptr;

    register ulong longword, himagic, lomagic;

    for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0; ++char_ptr)

    {

        if ( * char_ptr == '\0')

            return char_ptr - str;

    }

    longword_ptr = (ulong * )char_ptr;

    himagic = 0x80808080L;

    lomagic = 0x01010101L;

    while (1)

    {

        longword = * longword_ptr++;

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

        {

            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;

        }

    }

}

上面的代码与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算法类似

ASM Code
1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

        page    ,132

        title   strlen - return the length of a null-terminated string

;***

;strlen.asm - contains strlen() routine

;

;       Copyright (c) Microsoft Corporation. All rights reserved.

;

;Purpose:

;       strlen returns the length of a null-terminated string,

;       not including the null byte itself.

;

;*******************************************************************************

        .xlist

        include cruntime.inc

        .list

page

;***

;strlen - return the length of a null-terminated string

;

;Purpose:

;       Finds the length in bytes of the given string, not including

;       the final null character.

;

;       Algorithm:

;       int strlen (const char * str)

;       {

;           int length = 0;

;

;           while( *str++ )

;                   ++length;

;

;           return( length );

;       }

;

;Entry:

;       const char * str - string whose length is to be computed

;

;Exit:

;       EAX = length of the string "str", exclusive of the final null byte

;

;Uses:

;       EAX, ECX, EDX

;

;Exceptions:

;

;*******************************************************************************

        CODESEG

        public  strlen

strlen  proc \

        buf:ptr byte

        OPTION PROLOGUE:NONE, EPILOGUE:NONE

        .FPO    ( 0, 1, 0, 0, 0, 0 )

string  equ     [esp + 4]

        mov     ecx,string              ; ecx -> string

        test    ecx,3                   ; test if string is aligned on 32 bits

        je      short main_loop

str_misaligned:

        ; simple byte loop until string is aligned

        mov     al,byte ptr [ecx]

        add     ecx,1

        test    al,al

        je      short byte_3

        test    ecx,3

        jne     short str_misaligned

        add     eax,dword ptr 0         ; 5 byte nop to align label below

        align   16                      ; should be redundant

main_loop:

        mov     eax,dword ptr [ecx]     ; read 4 bytes

        mov     edx,7efefeffh

        add     edx,eax

        xor     eax,-1

        xor     eax,edx

        add     ecx,4

        test    eax,81010100h

        je      short main_loop

        ; found zero byte in the loop

        mov     eax,[ecx - 4]

        test    al,al                   ; is it byte 0

        je      short byte_0

        test    ah,ah                   ; is it byte 1

        je      short byte_1

        test    eax,00ff0000h           ; is it byte 2

        je      short byte_2

        test    eax,0ff000000h          ; is it byte 3

        je      short byte_3

        jmp     short main_loop         ; taken if bits 24-30 are clear and bit

                                        ; 31 is set

byte_3:

        lea     eax,[ecx - 1]

        mov     ecx,string

        sub     eax,ecx

        ret

byte_2:

        lea     eax,[ecx - 2]

        mov     ecx,string

        sub     eax,ecx

        ret

byte_1:

        lea     eax,[ecx - 3]

        mov     ecx,string

        sub     eax,ecx

        ret

byte_0:

        lea     eax,[ecx - 4]

        mov     ecx,string

        sub     eax,ecx

        ret

strlen  endp

        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

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

strlen源码剖析的更多相关文章

  1. strlen源码剖析(可查看glibc和VC的CRT源代码)

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

  2. 【转】strlen源码

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

  3. 菜鸟nginx源码剖析数据结构篇(三) 单向链表 ngx_list_t[转]

    菜鸟nginx源码剖析数据结构篇(三) 单向链表 ngx_list_t Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog.csd ...

  4. jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...

  5. Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

    声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...

  6. Apache Spark源码剖析

    Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著   ISBN 978-7-121-25420- ...

  7. 基于mybatis-generator-core 1.3.5项目的修订版以及源码剖析

    项目简单说明 mybatis-generator,是根据数据库表.字段反向生成实体类等代码文件.我在国庆时候,没事剖析了mybatis-generator-core源码,写了相当详细的中文注释,可以去 ...

  8. STL"源码"剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  9. SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现

    SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...

随机推荐

  1. CSS3中的弹性布局——&quot;em&quot;的用法

    使用CSS也好久了,但一直都是在使用“px”来设置Web元素的相关属性,未敢使用“em”.主要原因是,对其并不什么了解,只知道一点概念性的东西,前段时间在项目中要求使用“em”作为单位设置元素,所以从 ...

  2. SQL server2008-对象资源管理器

    对象资源管理器:数据库 .安全性.服务器对象.复制.管理 .SQL server代理 六部分组成

  3. mysql存储过程之异常处理篇

    mysql存储过程也提供了对异常处理的功能:通过定义HANDLER来完成异常声明的实现 语法如下: DECLARE handler_type HANDLER FOR condition_value[, ...

  4. http请求的referer属性

    HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理.比如从我主页上链 ...

  5. mysql 查询每个分组前N条记录

    mysql 查询每个分组前N条记录 假设存在表movie,  有字段 id, part(地区), mcount(观看次数) 现查询每个地区观看次数最多的3部movie, 则表 ###id虽未存在gro ...

  6. 【SSH进阶之路】Hibernate基本原理(一)

    在开始学Hibernate之前,一直就有人说:Hibernate并不难,无非是对JDBC进一步封装.一句不难,难道是真的不难还是眼高手低? 如果只是停留在使用的层面上,我相信什么技术都不难,看看别人怎 ...

  7. CSS Font-family常用设置

    font-family: "Avenir Next", Avenir, "Helvetica Neue", Helvetica, "Lantinghe ...

  8. SuperMap iObject入门开发系列之二地下三维管线系统介绍

    本文是一位好友“托马斯”授权给我来发表的,介绍都是他的研究成果,在此,非常感谢. 上次对超图平台组件式开发进行介绍,这次介绍的是基于这个框架开发的地下三维管线系统.地下管线涉及给水.雨水.污水.燃气. ...

  9. undefined symbol

    参考链接:  https://blog.csdn.net/shatterheart/article/details/52440149

  10. 微信小程序开发填坑指南V1

    近期用了一星期的时间,开发了一个小程序.小程序名称是:小特Jarvis,取自钢铁侠的管家. 后台采用C#编写,WebAPI接口.其实开发时间并不多,小程序本身提供的API,相比公众号的API来说,已经 ...