具有位操作的快速strlen

时间:2015-09-05 23:55:51

标签: c bitwise-operators strlen

我找到了这段代码

int strlen_my(const char *s)
{
    int len = 0;
    for(;;)
    {
        unsigned x = *(unsigned*)s;
        if((x & 0xFF) == 0) return len;
        if((x & 0xFF00) == 0) return len + 1;
        if((x & 0xFF0000) == 0) return len + 2;
        if((x & 0xFF000000) == 0) return len + 3;
        s += 4, len += 4;
    }
}

我很想知道它是如何工作的。 ¿任何人都可以解释它的工作原理吗?

6 个答案:

答案 0 :(得分:3)

它处理未定义的行为(未对齐的访问,75%的访问超出数组末尾的概率)以获得非常可疑的加速(很可能甚至更慢)。并且不符合标准,因为它返回int而不是size_t。即使平台上允许未对齐的访问,它们也可能比对齐访问慢得多。

它也不适用于大端系统,或unsigned不是32位。更不用说多重掩码和条件操作了。

那说:

它通过加载unsigned(甚至不保证超过16位)一次测试4个8位字节。一旦任何字节包含'\0' - 终止符,它将返回当前长度加上该字节位置的总和。否则,它将当前长度增加并行测试的字节数(4)并获得下一个unsigned

我的建议:优化的坏例子加上太多的不确定性/陷阱。它可能不会更快 - 只需根据标准版本进行分析:

size_t strlen(restrict const char *s)
{
    size_t l = 0;
    while ( *s++ )
        l++;
    return l;
}

可能有一种方法可以使用特殊的向量指令,但除非你能证明这是一个关键函数,否则你应该把它留给编译器 - 有些人可能会更好地展开/加速这样的循环。

答案 1 :(得分:3)

按位AND与1将从另一个操作数中检索位模式。含义,10101 & 11111 = 10101。如果该按位AND的结果为0,那么我们就知道另一个操作数为0.当使用0xFF(1)与单个字节进行AND运算时,结果为0将表示空字节。

代码本身在四字节分区中检查char数组的每个字节。 注意:此代码不可移植;在另一台机器或编译器上,unsigned int可能超过4个字节。使用uint32_t数据类型来确保32位无符号整数可能会更好。

首先要注意的是,在小端机器上,构成字符数组的字节将以相反的顺序读入无符号数据类型;也就是说,如果当前地址的四个字节是与abcd对应的位模式,则无符号变量将包含与dcba对应的位模式。

第二个是C中的十六进制数常量导致一个int大小的数字,在位模式的小端有指定的字节。这意味着,当使用4字节整数进行编译时,0xFF实际上是0x000000FF0xFF000x0000FF00。等等。

所以程序基本上是在四个可能的位置寻找NULL字符。如果当前分区中没有NULL字符,它将前进到下一个四字节插槽。

以char数组abcdef为例。在C中,字符串常量在结尾处始终具有空终止符,因此在该字符串的末尾有一个0x00字节。

它的工作原理如下:

将“abcd”读入unsigned int x:

x: 0x64636261 [ASCII representations for "dcba"]

检查每个字节是否为空终止符:

  0x64636261
& 0x000000FF
  0x00000061 != 0,

  0x64636261
& 0x0000FF00
  0x00006200 != 0,

检查其他两个位置;这个4字节分区中没有空终止符,所以前进到下一个分区。

将“ef”读入unsigned int x:

x: 0xBF006665 [ASCII representations for "fe"]

注意0xBF字节;这超过了字符串的长度,所以我们从运行时堆栈中读取垃圾。它可能是任何东西。在不允许未对齐访问的计算机上,如果字符串后面的内存不是1字节对齐,则会崩溃。如果字符串中只剩下一个字符,我们将读取两个额外的字节,因此与char数组相邻的内存对齐必须是2字节对齐。

检查每个字节是否为空终止符:

  0xBF006665
& 0x000000FF
  0x00000065 != 0,

  0xBF006665
& 0x0000FF00
  0x00006600 != 0,

  0xBF006665
& 0x00FF0000
  0x00000000 == 0 !!!

所以我们返回len + 2; len为4,因为我们将它一次递增4,所以我们返回6,这确实是字符串的长度。

答案 2 :(得分:3)

Code" works"通过假设字符串布局并且像int数组一样可访问,尝试一次读取4个字节。代码依次读取第一个int然后读取每个字节,测试它是否为空字符。理论上,使用char的代码运行速度会快于4个*(unsigned*)s个操作。

但有问题:

对齐是一个问题:例如if((x & 0xFF) == 0)可能会出错。

Endian是一个问题s可能无法获取地址s += 4处的字节

sizeof(int)是一个问题,因为int可能与4不同。

数组类型可能会超出size_t范围,最好使用#include <stddef.h> #include <stdio.h> static inline aligned_as_int(const char *s) { max_align_t mat; // C11 uintptr_t i = (uintptr_t) s; return i % sizeof mat == 0; } size_t strlen_my(const char *s) { size_t len = 0; // align while (!aligned_as_int(s)) { if (*s == 0) return len; s++; len++; } for (;;) { unsigned x = *(unsigned*) s; #if UINT_MAX >> CHAR_BIT == UCHAR_MAX if(!(x & 0xFF) || !(x & 0xFF00)) break; s += 2, len += 2; #elif UINT_MAX >> CHAR_BIT*3 == UCHAR_MAX if (!(x & 0xFF) || !(x & 0xFF00) || !(x & 0xFF0000) || !(x & 0xFF000000)) break; s += 4, len += 4; #elif UINT_MAX >> CHAR_BIT*7 == UCHAR_MAX if ( !(x & 0xFF) || !(x & 0xFF00) || !(x & 0xFF0000) || !(x & 0xFF000000) || !(x & 0xFF00000000) || !(x & 0xFF0000000000) || !(x & 0xFF000000000000) || !(x & 0xFF00000000000000)) break; s += 8, len += 8; #else #error TBD code #endif } while (*s++) { len++; } return len; }

试图解决这些困难。

{{1}}

答案 3 :(得分:2)

所有提案都比简单的strlen()慢。

原因是他们没有减少比较次数,只有一次涉及对齐。

检查来自Torbjorn Granlund(tege@sics.se)和Dan Sahlin(dan@sics.se)的strlen()提案。如果您使用的是64位平台,这确实有助于加快速度。

答案 4 :(得分:1)

它检测是否在little-endian机器上的特定字节设置了任何位。因为我们只检查一个字节(因为所有半字节,0或0xF都加倍)并且恰好是最后一个字节位置(因为机器是小端的,因此数字的字节模式是相反的)我们可以立即知道哪个字节包含NUL。

答案 5 :(得分:1)

循环每次迭代需要4个字节的char数组。四个if语句用于确定字符串是否结束,使用带有AND运算符的位掩码来读取所选子字符串的第i个元素的状态。