可以接受任意终止符的快速通用 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 循环退出条件。
memchr
比 strchr
便宜一些,它必须检查每个字节是否是潜在的 0
;展开和 SIMD 不会减少的工作量。如果 L1 缓存中的数据不热,那么在大多数 ISA 的大多数 CPU 上,一个好的 strchr 通常仍然可以跟上可用带宽。对于在您要查找的字节之前包含 0
字节的数组,检查 0
s 也是一个正确性问题。
如果可用,它甚至会使用 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
代码,但不会检查缓冲区的末尾。
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 循环退出条件。
memchr
比 strchr
便宜一些,它必须检查每个字节是否是潜在的 0
;展开和 SIMD 不会减少的工作量。如果 L1 缓存中的数据不热,那么在大多数 ISA 的大多数 CPU 上,一个好的 strchr 通常仍然可以跟上可用带宽。对于在您要查找的字节之前包含 0
字节的数组,检查 0
s 也是一个正确性问题。
如果可用,它甚至会使用 SIMD 指令一次检查 16 或 32 个字节。一个纯 C bithack(带有 strict-aliasing UB)就像你发现的那样只用于真实 C 库中的可移植后备代码(__attribute__((may_alias))
的类型定义。请参阅本段前面的 link。)
您当然不希望一次只检查 4 个字节的 bithack,特别是如果 unsigned long
是 64 位 CPU 上的 8 字节类型!
如果您不知道缓冲区长度,请在 C11 / C++17 中使用 len = -1
如果可用则使用rawmemchr
,否则使用memchr(ptr, c, -1)
.
这相当于传递 SIZE_MAX
.
见
保证不会读完匹配项,或者至少表现得好像没有读过,即没有错误。所以 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
代码,但不会检查缓冲区的末尾。