strlen源码分析

  说到 strlen,最直接最naive的算法,就是逐位比较是否为 '\0'

inline size_t my_strlen(const char * str) {
        register size_t len = 0;
        while (*str++ != 0)
                len++;
        return len;
}
  而一个简单的尝试就会知道这样的效率有多低。下表是自己实现的 my_strlen 函数与库函数都运行十万次的耗时比较。(CPU 24核*2.0GHz,64位,128G内存)


表1: strlen 与 my_strlen 效率对比
字符串长度strlen 运行时间(ms)my_strlen 运行时间(ms)
50.921.75
201.006.16
1001.7832.06
5006.75156.21


  效果不言而喻,对于短字符串来说,效率差得很少;但是对于长字符串来说,效率相差32倍左右。因此 glibc 中的 strlen 函数必须不是用这个简单的方法的。查看 源码发现,是通过两个magic number,每八位一组测试一次的方法进行测试。


size_t strlen (str)
const char *str;
{
	/* 
	 * 快速的搜索null结束标志。主要思想:
	 * 每4个字符一组(或8个,取决于运行平台的字长)进行判断。
	 * 判断'\0'的位置。只要是magic number的使用和意义。
	 */ 
	const char *char_ptr;
	const unsigned long int *longword_ptr;
	unsigned long int longword, himagic, lomagic;
	/* 
	 * 由于glic需要多平台移植,因此需要考虑非内存对齐的场景
	 * 后面是将四个连着的字符转换成long int,因此需要进行对齐。
	 * 这个循环用于对齐。
	 */
	for (char_ptr = str; ((unsigned long int) char_ptr
						& (sizeof (longword) - 1)) != 0;
    					++char_ptr)
		if (*char_ptr == '/0')
			return char_ptr - str;
	/*   
	 * 所有的概念设计都是针对4字节长的长字(双字), 
	 * 但对于8字节的双字同样适用。 
	 */ 
	longword_ptr = (unsigned long int *) char_ptr;

	/* 
	 * 魔法数字:
	 * bits:  01111110 11111110 11111110 11111111 
	 * bytes: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD
	 * 第31、24、16和8位为0,其余为1.这些为零的数字被称为“洞”。每个字节的左面
	 * 都有一个洞(前一个字节的最右面。)。
	 * 为1的位保证进位被传递到下一个0位中。
	 * 为0的位用来将进位放入其中。
	 */
	himagic = 0x80808080L;
	lomagic = 0x01010101L;
	if (sizeof (longword) > 4)
	{
		/* 64位的情况,分两次位移16位,是为了防止当long只有32位时出现警告。 */
		himagic = ((himagic << 16) << 16) | himagic;
		lomagic = ((lomagic << 16) << 16) | lomagic;
	}
	/* 高于64位,程序出错! */
	if (sizeof (longword) > 8)
		abort ();
	/*
	 * 通过每次测试一个长字(四个字符),替代传统的每次测试一个字符。
	 * 当这四个字符中任一个为null时,依次测试这四个字符。
	 */
	for (;;)  {
		/* 当把MAGIC_BITS加到LONGWORD时,没有改变LONGWORD中的任何一个hole,这时退出循环。
		 * 1) 能不能保证测试出所有的0bytes。
		 *    假设有一个全零的字节。任何来自其左侧的进位都会调进“洞”中,并停止
		 *    向高位继续传播。这样,这个全零字节就没有向高位字节的进位,那么,结
		 *    果中,对应的左侧的字节的最底位就不会改变。0就会被检测出来。
		 * 2) 是否会忽略所有的字节,除了全零字节。 
		 *    假设LONGWORD的每一个字节都有一个非零的位。有一个进位会进到 
		 *    8位。如果8位非零,进位会传递到16位。如果8位是零,9到15位中, 
		 *    有一个不是零,因此16位仍然有进位。同样,有进位到24位。如果24 
		 *    到30位有不是零的,31位就会有进位,所以,所有的洞都会改变。 
		 *    失败发生在LONGWORD的24到30位是0而31位是1.在这种情况下,31位 
		 *    的洞不会改变。如果我们可以操作处理器的进位标记,我们可以 
		 *    通过吧第四个洞放在32位来关闭这个枪眼。 
		 */
		longword = *longword_ptr++;
		/*
		 * 通过这个异或,可以把得到的和中洞的对应位进行设置。
		 * 如果没有从低位向“洞”的进位,那么异或后其值仍然是零;
		 * 如果没有进位,则其值为1.
		 * 这样,就可以知道后面的字节是否有全零的。
		 * (没有进位时,后面的字节为全零)
		 */
		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;
			/*处理64位的情况,也即每8个字符一组的比较情况*/
			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;
			}
		}
	}
}
  很明显,这个要比 my_strlen 运行的快。这个的原理如下: 

  如果有一个字节为全零,在进行减法的时候,这个字节会向高位字节借位,这样这个字节的最高位就变成了1。对于asc码,每个字节的最高位都是0,这样就可以检测出是否有全零的字节。但是对于最高位为1的字符(例如中文),上面的算法就很低效了。

=============

  补充:

  在 glibc 2.7 中,判断是否进位的代码为

if (((longword - lomagic) & himagic) != 0)
  而到了 glibc 2.10 中,代码改成了

if (((longword - lomagic) & ~longword & himagic) != 0)
  当每个字节最高位为1的时候,那个 “& ~longword” 就会把这种误判非零的 case 去掉,提高对于最高位为1的字符组成的字符串的判断效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值