学习高效编程的有效途径之一就是阅读高手写的源代码,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
2 3 4 5 6 7 |
size_t strlen_a(const char *str)
{ size_t length = 0 ; while (*str++ ) ++ length; return length; } |
也许可以稍加改进如下:
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
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; for (char_ptr = str; ((ulong)char_ptr 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 ) |
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;
测试代码如下:
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------------------------------->0magic_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------------------------------->0longword: 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
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; for (char_ptr = str; ((ulong)char_ptr & (sizeof(ulong) - 1)) != 0; ++char_ptr) if ( * char_ptr == '\0') 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) } } |
上面的代码与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
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 page ; ;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 \ OPTION PROLOGUE:NONE, EPILOGUE:NONE .FPO ( 0, 1, 0, 0, 0, 0 ) string equ [esp + 4] mov ecx,string ; ecx -> string str_misaligned: ; simple byte loop until string is aligned 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 byte_3: lea eax,[ecx - 1] lea eax,[ecx - 2] lea eax,[ecx - 3] lea eax,[ecx - 4] 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
--------------------------------------------------