说到strlen,想必这是C标准库中提供的字符串操作函数最简单的一个了,相信大家都会很快的写出下面的代码:
int strlen( const char* str )
{
int length = 0;
while ( *str++ )
++length;
return ( length );
}
或者如下代码:
int strlen( const char* str )
{
const char* ptr = str;
while ( *str++ )
;
return ( str - ptr - 1 );
}
但我们有没有想过这样做的效率如何呢?可不可以一次性判断几个字节来代替逐个字节判断这种传统的方式呢?就像memcpy那样,一次拷贝sizeof(unsigned long int)个字节一样。
果然glibc中采用了这种技巧。下面给出在32位计算机上的C语言实现(有改动, 假设unsigned long int 为4个字节):
size_t strlen(const char *str)
{
const char *char_ptr = NULL;
const unsigned long int *longword_ptr = NULL;
register unsigned long int longword, magic_bits;
for (char_ptr = str; ((unsigned long int)(char_ptr)
& (sizeof(unsigned long int) - 1)) != 0; ++char_ptr)
{
if (*char_ptr == '\0')
return char_ptr - str;
}
longword_ptr = (const unsigned long int *)char_ptr;
magic_bits = 0x7efefeffL;
for ( ;; )
{
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;
}
}
}
首先,我们理清程序的思路,可以归纳为如下三点:
1.C标准库要求有很好的移植性,在绝大部分系统体系结构下都应该能正确运行。那么每次拿出4个字节比较(unsigned long int),就需要考虑内存对齐问题,传入的字符串的首字符地址可不一定在4对齐的地址上。如果在内存对齐之前就遇到了'\0' 则直接return推出。否则到下一步;
2.一次读入并判断一个4字节(sizeof(unsigned long int))大小的内存块,如果4字节中没有为0的字节,则继续下一个4字节,否则到下一步;
3.到这里4字节中至少有一个字节为0,最后要做的就是找出这个字节的位置并return推出。
什么是数据对齐呢?
数据对齐(data alignment),是指所在的内存地址必须是该数据长度的整数倍,这样CPU的存取速度就会最快。比如在32位计算机中,一个int型的数据为4个字节,则int型数据的起始地址能被4整除的时候CPU的存取效率比较高。CPU的优化规则如下:对于n字节(n = 2, 4, 8...)的元素,它的首地址能被n整除才能获得最好的性能。
for (char_ptr = str; ((unsigned long int)(char_ptr)
& (sizeof(unsigned long int) - 1)) != 0; ++char_ptr)
{
if (*char_ptr == '\0')
return char_ptr - str;
}
上面这段代码正是实现了数据对齐,完成了上述的第1点。
因为char_ptr已经4字节对齐,所以
longword_ptr = (const unsigned long int *)char_ptr;
中的强制转换是安全的!
在接着分析之前,我们先来认识一下程序中magic_bits这个魔数,我们将它按bit展
0x7efefeff:01111110-11111110-11111110-11111111
我们注意到有4个红色的0,它们的位置被称为holes位,它们都有一个特征,就是在每个字节的左边,这有什么用呢?再联想一下,在左边,什么时候会被修改? 很明显,当右边有进位时,会修改到这个0,或者这几个0的位置与另外一个数相运算时会被改变。
下面着重分析:
(((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0
我们将((longword + magic_bits) ^ ~longword) & ~magic_bits语句以运算符&为基准分成两部分来看:左表达式 ((longword + magic_bits) ^ ~longword) 和
右表达式 ~magic_bits。有点编程经验的都知道在if判断语句中&的用处是用于判断 左表达式 中相应位是1还是0,那么相应位是指哪些位呢?相应位指的是与 右表达式中为1的位 相同的位。举个例子吧:
假设我有个int型的数据ival, 现在我们要判断ival的第15位是1还是0,那么我们可以这样写:
if (ival & (1 << 15))
{
// do something
}
如果第15位是1,那么ival & (1 << 15)为真,否则为假。
~magic_bits的二进制形式为: 10000001-00000001-00000001-00000000,可以看出为1的位置正是相应holes的位置,那么可以得出
(((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0的作用是在判断((longword + magic_bits) ^ ~longword)中的相应holes为是0还是1,由最后的!=0得知当
((longword + magic_bits) ^ ~longword)中相应holes位置一个或多个为1时,进入if判断语句块。
那么现在的问题是((longword + magic_bits) ^ ~longword)是什么意思呢?
我们假设result = longword + magic_bits, 那么result中相应holes位置的值就会根据longword的实际值而设置:如果longword中某个字节是0,那么与magic_bits对应字节相加就不会产生进位,那么result中相应holes位置就不会设置为0; 当longword中某个字节是非零时,结果恰恰相反-- 与magic_bits对应字节相加会产生进位,则result中相应holes位置就会设置为1。
最后一个问题是:result ^ ~longword是什么意思呢?
我们假设a = result ^ ~longword。
我们举个具体的例子吧,如下(注意:longword中的各个字节的最高位不可能为1,因为ascii码范围是[0-127])
那么if ((a & b) != 0)什么意思呢?检测a中的holes位是1还是0.
现在终于知道只要longword中有一个或几个为0的字节,就可以进入if判断语句块中,这正是strlen所做的 -- strlen就是为了找到结束符,然后返回长度 ^_^。
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 ;
cp指向最后一个没有0字节的数据块,当cp[0] == 0时,即b0是00000000,那么字符串的长度为cp - str。
当cp[1] == 0时,即b1是00000000,那么字符串的长度除了cp - str外还需加上b0这个字节。
其他的同理。
glibc中关于strlen还有另一种实现,如下代码来自glibc2.10
#include <string.h>
#include <stdlib.h>
#undef strlen
/* Return the length of the null-terminated string STR. Scan for
the null terminator quickly by testing four bytes at a time. */
size_t
strlen (str)
const char *str;
{
const char *char_ptr;
const unsigned long int *longword_ptr;
unsigned long int longword, himagic, lomagic;
/* Handle the first few characters by reading one character at a time.
Do this until CHAR_PTR is aligned on a longword boundary. */
for (char_ptr = str; ((unsigned long int) char_ptr
& (sizeof (longword) - 1)) != 0;
++char_ptr)
if (*char_ptr == '\0')
return char_ptr - str;
/* All these elucidatory comments refer to 4-byte longwords,
but the theory applies equally well to 8-byte longwords. */
longword_ptr = (unsigned long int *) char_ptr;
/* Bits 31, 24, 16, and 8 of this number are zero. Call these bits
the "holes." Note that there is a hole just to the left of
each byte, with an extra at the end:
bits: 01111110 11111110 11111110 11111111
bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD
The 1-bits make sure that carries propagate to the next 0-bit.
The 0-bits provide holes for carries to fall into. */
himagic = 0x80808080L;
lomagic = 0x01010101L;
if (sizeof (longword) > 4)
{
/* 64-bit version of the magic. */
/* Do the shift in two steps to avoid a warning if long has 32 bits. */
himagic = ((himagic << 16) << 16) | himagic;
lomagic = ((lomagic << 16) << 16) | lomagic;
}
if (sizeof (longword) > 8)
abort ();
/* Instead of the traditional loop which tests each character,
we will test a longword at a time. The tricky part is testing
if *any of the four* bytes in the longword in question are zero. */
for (;;)
{
longword = *longword_ptr++;
if (((longword - lomagic) & ~longword & himagic) != 0)
{
/* Which of the bytes was the zero? If none of them were, it was
a misfire; continue the search. */
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;
if (sizeof (longword) > 4)
{
if (cp[4] == 0)
return cp - str + 4;
if (cp[5] == 0)
return cp - str + 5;
if (cp[6] == 0)
return cp - str + 6;
if (cp[7] == 0)
return cp - str + 7;
}
}
}
}
libc_hidden_builtin_def (strlen)
其他的道理都一样,我们再重点分析这一句:
if (((longword - lomagic) & ~longword & himagic) != 0)
我们分两部分看。
第一部分(long - lomagic)的作用是判断longword中每个字节的自高位:如果longword中任何一个字节等于0,那么想减后最高位为由0编程1。其他的(小于128并且大于0)最高位则不变(依然为0)。
第二部分~longword & himagic的作用是判断longword中每个字节最高位的设置。只要longword中任何一个字节大于等于0并且小于128,则结果就是himagic。
两部分相与的意思就是判断holes位有没有是1的,只要有1,证明有字节为0。
这里需要提的是:在glibc2.7中该判断语句直接是
if (((longword - lomagic) & himagic) != 0)
这个更好理解一些! 可是为什么后来改成
if (((longword - lomagic) & ~longword & himagic) != 0)
我试了几个实际值,发现后一种可以很好的过滤掉longword的值大于等于128的情况。这也许就是更改的原因吧,只是猜测!
好了,strlen源码分析就到这里吧。也许我们发现glibc的可读性很差,但是效率却是无库可及啊!弄懂了strlen的实现原理,那么glibc中很多其他函数就很容易懂了 ^_^
参考链接:
http://www.cppblog.com/ant/archive/2007/10/12/32886.html
http://blog.youkuaiyun.com/masefee/article/details/7040012
http://blog.youkuaiyun.com/ken_2642/article/details/1958347