学习高效编程的有效途径之一就是阅读高手写的源代码,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. Android的应用程序的异常处理2

    1.自定义一个类(MaApp)继承Application 2.在清单文件中的Application选项菜单对应的name属性中添加MyApp 3.重写application中的onCreate方法 4 ...

  2. javascript DOM,它到底是什么-------Day32

    这一晚上看的我是头疼不已啊,为什么呢? 终究是半路出家,我对javascript的理解仅仅停留在:调用javascript,改变页面样式,元素和实现一些事件的响应,尽管须要的时候可能会用,可是到底使用 ...

  3. js 取消listbox选中的项

    <input type="button" id="cel" value="取消选择" onclick="clearListB ...

  4. Ext.Net 使用总结之查询条件中的起始日期

    2.关于查询条件中起始日期的布局方式 首先上一张图,来展示一下我的查询条件的布局,如下: 大多数时候,我们的查询条件都是一个条件占一个格子,但也有不同的时候,如:查询条件是起始日期,则需要将这两个条件 ...

  5. linux查看与开启ssh

    首先通过物理终端进入到linux上,手工检查ssh发现没运行/etc/init.d/sshd statussshd is stopped手工启动服务,发现报告权限错误./etc/init.d/sshd ...

  6. C++ try catch 捕获空指针异常,数组越界异常

    #include <exception> #include <iostream> using namespace std; /************************* ...

  7. VC++学习之GDI概述

    VC++学习之GDI概述 图形设备接口(GDI)是一个可执行程序,它接受Windows应用程序的绘图请求(表现为GDI函数调用),并将它们传给相应的设备驱动程序,完成特定于硬件的输出,象打印机输出和屏 ...

  8. Ubuntu 14.04 上使用 Nginx 部署 Laravel

    本教程将会涉及以下工具: Ubuntu 14.04 LTS PHP 5.5 MySQL Laravel 5.0 Nginx 参考文章:Ubuntu 14.04 上使用 Nginx 部署 Laravel ...

  9. List<T>到XML的序列化与反序列化

    原文 http://hi.baidu.com/shishengli1234/item/f0536f36335390c22f8ec24b Model.RESE_SCH sch1 = new Model. ...

  10. Java知识点复习

    总结下java的知识点 final 关键字-方法:不能被子类重写(override)-变量:不能被修改-类:不可以被继承,派生子类 finally 关键字与try/catch语句配合使用,即使有异常抛 ...