可以接受任意终止符的快速通用 strlen() 实现

Fast generic strlen() implementation that can accept arbitrary terminator character

template <char terminator = '[=10=]'>
size_t strlen(const char *str)
{
    const char *char_ptr;
    const unsigned long int *longword_ptr;
    unsigned long int longword, magic_bits, himagic, lomagic;

    for (char_ptr = str; ((unsigned long int) char_ptr 
             & (sizeof (longword) - 1)) != 0; ++char_ptr)
       if (*char_ptr == '[=10=]')
           return char_ptr - str;

    longword_ptr = (unsigned long int *) char_ptr;

    himagic = 0x80808080L;
    lomagic = 0x01010101L;

    for (;;)
    { 
        longword = *longword_ptr++;

        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;
        }
    }
}

以上是glibc strlen()代码。它依靠 Determine if a word has a zero byte 的技巧来使其快速。

但是,我希望使用模板使该函数适用于任何终止符,而不仅仅是 '[=12=]'。是否可以做类似的事情?

使用std::memchr利用libc的hand-written asm

它returns一个指向找到的字节的指针,所以你可以通过减去得到长度。它 returns NULL 在 not-found 上,但你说你可以假设会有匹配,所以我们不需要检查,除非作为调试断言。

更好的是,如果您可以假设 GNU 函数可用,请使用 rawmemchr,这样您甚至不必传递长度。

#include <cstring>

size_t lenx(const char *p, int c, size_t len)
{
    const void *match = std::memchr(p, c, len);  // old C functions take char in an int
    return static_cast<const char*>(match) - p;
}

现代主流 CPU 的任何体面的 libc 实现都将有一个快速 memchr 实现,可以一次检查多个字节,通常 hand-written 在 asm 中。与 strlen 实现非常相似,但在展开的循环中具有 length-based 循环退出条件,而不仅仅是 match-finding 循环退出条件。

memchrstrchr 便宜一些,它必须检查每个字节是否是潜在的 0;展开和 SIMD 不会减少的工作量。如果 L1 缓存中的数据不热,那么在大多数 ISA 的大多数 CPU 上,一个好的 strchr 通常仍然可以跟上可用带宽。对于在您要查找的字节之前包含 0 字节的数组,检查 0s 也是一个正确性问题。

如果可用,它甚至会使用 SIMD 指令一次检查 16 或 32 个字节。一个纯 C bithack(带有 strict-aliasing UB)就像你发现的那样只用于真实 C 库中的可移植后备代码( 解释了这一点并且有一些 links 到 glibc 的 asm 实现),或者在它编译成 asm 的目标上,可以像手写的那样好(例如 glibc 的 MIPS)。 (但是被包装在一个库函数中,strict-aliasing UB 是通过某种方式处理的,也许就像不能内联到以不同方式访问该数据的其他代码一样简单。如果你想做你自己,你会想要一个类似于 GNU C __attribute__((may_alias)) 的类型定义。请参阅本段前面的 link。)

您当然不希望一次只检查 4 个字节的 bithack,特别是如果 unsigned long 是 64 位 CPU 上的 8 字节类型!


如果您不知道缓冲区长度,请在 C11 / C++17 中使用 len = -1

如果可用则使用rawmemchr,否则使用memchr(ptr, c, -1).
这相当于传递 SIZE_MAX.

保证不会读完匹配项,或者至少表现得好像没有读过,即没有错误。所以 just like an optimized strlen, and probably for performance reasons not reading into the next cache line. (At least since C++17 / C11, according to cppreference,但实际实现几乎肯定可以安全地使用这种方式更长时间,至少出于性能原因。)

ISO C++ 标准本身 defers<cstring> 函数的 C 标准; C++17 及更高版本遵循 C11,它增加了 C99 没有的要求。 (我也不知道是否有 real-world 实施违反了该标准;我猜不会,更多的是记录实际实施已经在做的保证。)

POSIX man page for memchr 保证在比赛中停止;我不知道这种保证对 POSIX 系统有多远。

Implementations shall behave as if they read the memory byte by byte from the beginning of the bytes pointed to by s and stop at the first occurrence of c (if it is found in the initial n bytes).

如果没有这样的保证,假设一个实现可能只使用从您传递给它的地址开始的未对齐加载,只要它离您告诉它的缓冲区的 ptr[size-1] 端足够远关于。不过,出于性能原因,这不太可能。


rawmemchr()

如果您使用的是 GNU 系统,glibc 具有 rawmemchr,它假设会有一个匹配项,而不是大小限制。所以它可以像 strlen 一样循环,没有基于长度的第二个退出条件或检查每个字节的 0 以及给定的字符。

有趣的事实:AArch64 glibc implements it 作为 memchr(ptr, c, -1),或者如果字符恰好是 0,则作为 strlen。在其他 ISA 上,它实际上可能会复制 memchr 代码,但不会检查缓冲区的末尾。