您的位置:首页 > 编程语言 > C语言/C++

strlen源码剖析

2013-05-15 13:09 246 查看
转自-http://www.cppblog.com/ant/archive/2007/10/12/32886.html

 

学习高效编程的有效途径之一就是阅读高手写的源代码,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(constchar*str)
{
2 size_t length=0;
3 while (*str++)
4 ++length;
5 returnlength;
6 }
 

也许可以稍加改进如下:

 

1
size_t strlen_b(constchar*str)
{
2 constchar*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
3size_t strlen_c(constchar*str)
{
4
5constchar*char_ptr;
6 const ulong*longword_ptr;
7 register ulong longword, magic_bits;
8
9for (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
16longword_ptr= (ulong*)char_ptr;
17
18magic_bits=0x7efefeffL;
19
20while (1)
{
21
22longword=*longword_ptr++;
23
24if ((((longword+
magic_bits)^~longword)&~magic_bits)!=0)
{
25
26constchar*cp=
(constchar*)(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: 01111110111111101111111011111111

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:01111110111111101111111011111111
上面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:

constchar*cp=
(constchar*)(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(constchar*str)
{
2
3constchar*char_ptr;
4 const ulong*longword_ptr;
5 register ulong longword, himagic, lomagic;
6
7for (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
14longword_ptr= (ulong*)char_ptr;
15
16himagic=0x80808080L;
17 lomagic
= 0x01010101L;
18
19while (1)
{
20
21longword=*longword_ptr++;
22
23if (((longword-
lomagic) & himagic)!=0)
{
24
25constchar*cp=
(constchar*)(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: 00000001000000010000000100000001

在这种方法中假设所有字符都是ASCII标准字符,其值在0-127之间,因此longword总是如下形式:

b3 b2 b1 b0
31------------------------------->0
longword: 0XXXXXXX0XXXXXXX0XXXXXXX0XXXXXXX
 

检测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
2title strlen-return
the length of anull-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 anull-terminated
string,
10 ; not including thenullbyte
itself.
11 ;
12 ;*******************************************************************************
13
14.xlist
15 include cruntime.inc
16 .list
17
18page
19 ;***
20;strlen-return
the length of anull-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 (constchar*
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 thefinalnullbyte
42;
43 ;Uses:
44 ; EAX, ECX, EDX
45 ;
46 ;Exceptions:
47 ;
48 ;*******************************************************************************
49
50CODESEG
51
52public strlen
53
54strlen proc \
55 buf:ptr
byte
56
57OPTION PROLOGUE:NONE, EPILOGUE:NONE
58
59.FPO (0,1,0,0,0,0
)
60
61string equ [esp+4]
62
63mov ecx,string ; ecx-> string
64 test ecx,3 ; testif string is aligned on32
bits
65 je
short main_loop
66
67str_misaligned:
68 ; simplebyte loop until string is aligned
69 mov al,byte ptr [ecx]
70 add ecx,1
71test al,al
72 je
short byte_3
73 test ecx,3
74jneshort str_misaligned
75
76add eax,dword ptr0 ;5byte
nop to align label below
77
78align16 ; should be redundant
79
80main_loop:
81 mov eax,dword ptr [ecx] ; read4 bytes
82 mov edx,7efefeffh
83 add edx,eax
84 xor eax,-1
85xor eax,edx
86 add ecx,4
87test eax,81010100h
88 je
short main_loop
89 ; found zerobyte in the loop
90 mov eax,[ecx-4]
91 test al,al ; is itbyte0
92jeshort byte_0
93 test ah,ah ; is itbyte1
94jeshort byte_1
95 test eax,00ff0000h ; is itbyte2
96jeshort byte_2
97 test eax,0ff000000h ; is itbyte3
98jeshort byte_3
99 jmp
short main_loop ; takenif bits24-30
are clear and bit
100 ;
31 is set
101
102byte_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
123strlen endp
124
125end

6. 测试结果

为了对上述各种实现的效率有一个大概的认识,我在VC8和GCC下分别进行了测试,测试时均采用默认优化方式。下面是在GCC下运行几百万次后的结果(在VC8下的运行结果与此相似):

strlen_a
--------------------------------------------------
1:515 ticks0.515
seconds
2: 375 ticks0.375
seconds
3: 375 ticks0.375
seconds
4: 375 ticks0.375
seconds
5: 375 ticks0.375
seconds

total: 2015 ticks2.015 seconds

average: 403 ticks0.403 seconds
--------------------------------------------------

strlen_b
--------------------------------------------------
1:360 ticks0.36
seconds
2: 390 ticks0.39
seconds
3: 375 ticks0.375
seconds
4: 360 ticks0.36
seconds
5: 375 ticks0.375
seconds

total: 1860 ticks1.86 seconds

average: 372 ticks0.372 seconds
--------------------------------------------------

strlen_c
--------------------------------------------------
1:187 ticks0.187
seconds
2: 172 ticks0.172
seconds
3: 187 ticks0.187
seconds
4: 187 ticks0.187
seconds
5: 188 ticks0.188
seconds

total: 921 ticks0.921 seconds

average: 184 ticks0.1842 seconds
--------------------------------------------------

strlen_d
--------------------------------------------------
1:172 ticks0.172
seconds
2: 187 ticks0.187
seconds
3: 172 ticks0.172
seconds
4: 187 ticks0.187
seconds
5: 188 ticks0.188
seconds

total: 906 ticks0.906 seconds

average: 181 ticks0.1812 seconds
--------------------------------------------------

strlen
--------------------------------------------------
1:187 ticks0.187
seconds
2: 172 ticks0.172
seconds
3: 188 ticks0.188
seconds
4: 172 ticks0.172
seconds
5: 187 ticks0.187
seconds

total: 906 ticks0.906 seconds

average: 181 ticks0.1812 seconds
--------------------------------------------------

源代码:点击下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 源码 strlen