更快的“搜索单个匹配字符”(ala strchr
)算法。
重要笔记:
这些函数使用“(尾数)(尾数)的数量/计数” gcc
编译器内在函数- __builtin_ctz
。这些功能可能仅在具有执行该操作的指令的机器(即x86,ppc,arm)上很快。
这些功能假定目标体系结构可以执行32位和64位未对齐负载。如果您的目标体系结构不支持此功能,则需要添加一些启动逻辑以正确对齐读取。
这些功能与处理器无关。如果目标CPU具有矢量指令,则可能会做得更好。例如,strlen
下面的函数使用SSE3,可以对其进行微不足道的修改,以对所扫描的字节进行XOR运算以查找除以外的字节0
。在运行Mac OS X 10.6(x86_64)的2.66GHz Core 2笔记本电脑上执行的基准测试:
- 843.433 MB /秒
strchr
- 每秒2656.742 MB
findFirstByte64
- 每秒13094.479 MB
strlen
... 32位版本:
#ifdef __BIG_ENDIAN__
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (_x == 0u) ? 0 : (__builtin_clz(_x) >> 3) + 1; })
#else
#define findFirstZeroByte32(x) ({ uint32_t _x = (x); _x = ~(((_x & 0x7F7F7F7Fu) + 0x7F7F7F7Fu) | _x | 0x7F7F7F7Fu); (__builtin_ctz(_x) + 1) >> 3; })
#endif
unsigned char *findFirstByte32(unsigned char *ptr, unsigned char byte) {
uint32_t *ptr32 = (uint32_t *)ptr, firstByte32 = 0u, byteMask32 = (byte) | (byte << 8);
byteMask32 |= byteMask32 << 16;
while((firstByte32 = findFirstZeroByte32((*ptr32) ^ byteMask32)) == 0) { ptr32++; }
return(ptr + ((((unsigned char *)ptr32) - ptr) + firstByte32 - 1));
}
...和64位版本:
#ifdef __BIG_ENDIAN__
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (_x == 0ull) ? 0 : (__builtin_clzll(_x) >> 3) + 1; })
#else
#define findFirstZeroByte64(x) ({ uint64_t _x = (x); _x = ~(((_x & 0x7F7F7F7F7f7f7f7full) + 0x7F7F7F7F7f7f7f7full) | _x | 0x7F7F7F7F7f7f7f7full); (__builtin_ctzll(_x) + 1) >> 3; })
#endif
unsigned char *findFirstByte64(unsigned char *ptr, unsigned char byte) {
uint64_t *ptr64 = (uint64_t *)ptr, firstByte64 = 0u, byteMask64 = (byte) | (byte << 8);
byteMask64 |= byteMask64 << 16;
byteMask64 |= byteMask64 << 32;
while((firstByte64 = findFirstZeroByte64((*ptr64) ^ byteMask64)) == 0) { ptr64++; }
return(ptr + ((((unsigned char *)ptr64) - ptr) + firstByte64 - 1));
}
编辑2011/06/04 OP在注释中指出此解决方案具有“无法克服的错误”:
它可以读取经过查找的字节或空终止符,从而可以访问未映射的页面或未经读取许可的页面。除非对齐,否则根本无法在字符串函数中使用大读取。
从技术上讲,这是正确的,但实际上适用于对大于单个字节的块进行操作的任何算法,包括OP在注释中建议的方法:
一个典型的strchr
实现不是幼稚的,但是比您给出的要有效得多。有关最广泛使用的算法,请参见本文结尾:http : //graphics.stanford.edu/~seander/bithacks.html#ZeroInWord
实际上,它与对齐本身也没有任何关系。没错,这可能会导致使用的大多数常见体系结构上讨论的行为,但这与微体系结构的实现细节有关—如果未对齐的读取跨越4K边界(再次,典型),则该读取将导致程序如果未映射下一个4K页面边界,则终止错误。
但这不是答案中给出的算法中的“错误”-行为是因为函数喜欢strchr
并且strlen
不接受length
参数来限制搜索的大小。char bytes[1] = {0x55};
出于我们的讨论目的,搜索恰好位于4K VM页面边界的最末端,而下一个页面未映射,使用strchr(bytes, 0xAA)
(其中strchr
是一个字节一次的实现)将完全崩溃。同样的方法。strchr
相关表兄同上strlen
。
没有length
参数,就无法确定何时应退出高速算法并返回逐字节算法。更可能的“错误”是读取“过去的分配大小”,从技术上来说,这是undefined behavior
根据各种C语言标准得出的,并且会通过诸如标记为错误valgrind
。
总之,任何大于字节块的操作都可以更快地运行,因为这会回答代码和OP指出的代码,但必须具有字节精确的读取语义,如果没有length
参数,控制“最后读取”的特殊情况。
该答案中的代码是一个内核,用于在目标CPU具有类似快速ctz
指令的情况下快速找到自然CPU字长块中的第一个字节。添加诸如确保其仅在正确对齐的自然边界或某种形式的边界上运行这样的琐事length
,这将使您可以退出高速内核,而转入较慢的逐字节检查。
OP在评论中还指出:
至于您的ctz优化,它仅对O(1)尾部操作有所不同。它可以使用细小的字符串来提高性能(例如,strchr("abc", 'a');
但肯定不能用任何大号的字符串来提高性能)。
这种说法是否正确,很大程度上取决于所讨论的微体系结构。使用规范的4级RISC管道模型,则几乎可以肯定是正确的。但是,很难说出这对于现代乱序的超标量CPU是否正确,在这种情况下,内核速度会完全使内存流速度相形见.。在这种情况下,“可以退休的指令数”相对于“可以流传输的字节数”之间有很大的差距,这不仅是合理的,而且是很常见的。可以流传输的每个字节可以撤消的指令数”。如果足够大,则ctz
可以“免费” 执行+移位指令。