【转】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行代码:
- size_t strlen_a(const char *str) {
 - size_t length = 0;
 - while (*str++)
 - ++length;
 - return length;
 - }
 
也许可以稍加改进如下:
- 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个字节):
- 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;
第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)
 
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:
- 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. 另一种实现
- 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
lomagic = 0x01010101L;
以及 if语句变得比较简单了
if语句中的计算可以分为如下2步:
(1) longword - lomagic
himagic和lomagic的二进制表示如下:
31------------------------------->0
himagic: 10000000 10000000 10000000 10000000
lomagic: 0000000100000001 00000001 00000001
在这种方法中假设所有字符都是ASCII标准字符,其值在0-127之间,因此longword总是如下形式:
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算法类似
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下的运行结果与此相似):
--------------------------------------------------
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
【转】strlen源码的更多相关文章
- strlen源码剖析(可查看glibc和VC的CRT源代码)
		
学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简单的 ...
 - 库函数strlen源码重现及注意问题
		
首先直接上源码: size_t strlen (const char * str) { const char *eos = str; while(*eos++); return(eos - str - ...
 - strlen源码剖析
		
学习高效编程的有效途径之一就是阅读高手写的源代码,CRT(C/C++ Runtime Library)作为底层的函数库,实现必然高效.恰好手中就有glibc和VC的CRT源代码,于是挑了一个相对简 ...
 - [PHP源码阅读]strlen函数
		
文章来自:http://www.hoohack.me/2016/02/22/phps-source-analytics-strlen 我在github有对PHP源码更详细的注解.感兴趣的可以围观一下, ...
 - strlen、strcpy和strcmp源码
		
1.不使用库函数实现strcpy #include <assert.h> char *strcpy(char *dst, const char *src) { assert((dst != ...
 - 【原】FMDB源码阅读(二)
		
[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...
 - SDWebImage源码解读之SDWebImageCache(下)
		
第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...
 - [PHP源码阅读]explode和implode函数
		
explode和implode函数主要用作字符串和数组间转换的操作,比如获取一段参数后根据某个字符分割字符串,或者将一个数组的结果使用一个字符合并成一个字符串输出.在PHP中经常会用到这两个函数,因此 ...
 - 【原】SDWebImage源码阅读(五)
		
[原]SDWebImage源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 前面的代码并没有特意去讲SDWebImage的缓存机制,主要是想单独开一章节专门讲 ...
 
随机推荐
- http 状态码 40x
			
400 无法解析此请求. 401.1 未经授权:访问由于凭据无效被拒绝. 401.2 未经授权: 访问由于服务器配置倾向使用替代身份验证方法而被拒绝. 401.3 未经授权:访问由于 ACL 对所请求 ...
 - 使用openstackclient调用Keystone v3 API
			
本文内容属于个人原创,转载务必注明出处: http://www.cnblogs.com/Security-Darren/p/4138945.html 考虑到Keystone社区逐渐弃用第二版身份AP ...
 - OpenOffice将MS docx转换成pdf文件偶数页眉不显示问题解决办法
			
OpenOffice版本:4.0(Windows.Linux下测试都出现问题) MS Office版本:2007 问题描述 使用OpenOffice将MS的docx文件转换为pdf文件时,docx文件 ...
 - Python资料收藏(杂乱版)
			
http://blog.csdn.net/vagrxie/article/category/343814 http://blog.sina.com.cn/s/articlelist_280149524 ...
 - perl 查看某个模块的安装路径
			
perldoc 命令可以查看某个模块的安装路径 命令: perldoc -l Bio::Seq 返回值如下: /usr/local/share/perl5/Bio/Seq.pm
 - utf8   ucs4
			
这个问题不好回答,首先UTF-8编码只不过是一种Unicode的转换,兼容ASCII.所以,UTF-8编码支持的最大字符编码应该是Unicode支持的最大字符编码. 理论上,UTF-8编码可以支持最大 ...
 - unity3d 使用GL 方式画线
			
这个是画线部分 private Vector3[] linePoints; public int m_LineCount; public int m_PointUsed; public void Re ...
 - MySql数据库恢复(*frm)文件
			
mysql数据库恢复(*frm)文件 WorkBench 在使用虚拟服务器时,服务器提供商一般不会像我们使用本地数据库一样:使用导入导出(这样的文件后缀是*.sql).大部分时候提供的是一个文件夹,里 ...
 - 【DL】物体识别与定位
			
https://cloud.tencent.com/community/article/414833
 - vue实现点击关注之后及时更新列表
			
如图,我要实现点击关注之后列表及时更新成最新的列表. 思路很简单,主要是两点: 1.在点击关注之后去执行一个请求新的关注列表的action: 2.在vue组件中watch监听已关注列表和推荐关注列表 ...