说到 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内存)
字符串长度 | strlen 运行时间(ms) | my_strlen 运行时间(ms) |
---|---|---|
5 | 0.92 | 1.75 |
20 | 1.00 | 6.16 |
100 | 1.78 | 32.06 |
500 | 6.75 | 156.21 |
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的字符组成的字符串的判断效率。