glibc -- strlen源码分析

说到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 int4个字节):

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展

0x7efefeff01111110-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])


可以看出a中为1的位的意义就是result相对longword中的未改变的位。就是说result的某一位与longword中的相同位相同时,那么在a中的相对位就为1。
那么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.csdn.net/masefee/article/details/7040012

http://blog.csdn.net/ken_2642/article/details/1958347

 

                   


  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
glibc(GNU C Library)是Linux操作系统中的C语言库,提供了许多函数和组件供开发人员使用。在某些情况下,用户或开发人员可能需要将glibc-2.14降级到glibc-2.12。这可能是因为某些软件或应用程序与glibc-2.12兼容,但在glibc-2.14上运行出现问题。 要将glibc-2.14降级到glibc-2.12,首先需要查找并下载glibc-2.12的代码。可以从官方网站或其他可信的软件仓库中获取代码。然后,需要进行编译和安装。 在安装glibc-2.12之前,可能需要卸载或删除当前系统中已安装的glibc-2.14版本。卸载glibc-2.14可能会导致系统不稳定或无法启动,因此在执行此操作之前应备份系统或确认可恢复系统状态。 编译和安装glibc-2.12的过程可能会有一些复杂性,并需要具备一定的编程知识和技能。首先,需要进入glibc-2.12代码所在的目录,并执行`configure`命令以生成编译环境。然后,使用`make`命令编译代码,并使用`make install`命令安装glibc-2.12。 安装完成后,可能需要进行一些配置,例如更新动态链接器或库的路径。这样确保系统在运行时使用glibc-2.12而不是glibc-2.14。 需要注意的是,将glibc-2.14降级到glibc-2.12可能会导致系统稳定性或兼容性问题。降级glibc可能会影响系统中其他软件的功能和性能。因此,在执行此操作之前,建议先了解软件或应用程序对glibc版本的要求,并评估降级是否是唯一的解决方案。 总之,将glibc-2.14降级到glibc-2.12需要下载、编译和安装glibc-2.12的代码,并进行一些系统配置。但需要谨慎操作,确保降级前后的系统稳定性和兼容性。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值