strstr 比算法快?
Posted
技术标签:
【中文标题】strstr 比算法快?【英文标题】:strstr faster than algorithms? 【发布时间】:2011-11-27 01:40:57 【问题描述】:我有一个 21056 字节的文件。
我用 C 语言编写了一个程序,将整个文件读入缓冲区,然后使用多种搜索算法在文件中搜索 82 个字符的标记。
我已经使用了“Exact String Matching Algorithms” 页面中算法的所有实现。我使用过:KMP、BM、TBM 和 Horspool。然后我使用了strstr
并对每一个进行了基准测试。
我想知道的是,每次strstr
的性能都优于所有其他算法。有时唯一更快的是BM。
strstr
不应该是最慢的吗?
这是我的基准代码以及基准测试 BM 的示例:
double get_time()
LARGE_INTEGER t, f;
QueryPerformanceCounter(&t);
QueryPerformanceFrequency(&f);
return (double)t.QuadPart/(double)f.QuadPart;
before = get_time();
BM(token, strlen(token), buffer, len);
after = get_time();
printf("Time: %f\n\n", after - before);
有人可以向我解释为什么strstr
的性能优于其他搜索算法吗?如果需要,我会根据要求发布更多代码。
【问题讨论】:
错误在您的代码中。至少 Horspool 应该始终优于strstr
- KMP 实际上可能更慢。但由于您没有发布代码,我们无法帮助您。也就是说,您可以故意选择您的数据以使天真的搜索获胜,因此输入数据的选择也很重要。
您是否认真地对在 20k 字符串中搜索 80 字节字符串进行基准测试?...您的样本量太小了,您可以手动完成!
【参考方案1】:
为什么你认为strstr
应该比其他所有的都慢?你知道strstr
使用什么算法吗?我认为strstr
很可能使用KMP
类型或更好的微调、特定于处理器的汇编编码算法。在这种情况下,你不可能在C
这样小的基准测试中表现得更好。
(我认为这可能是因为程序员喜欢实现这样的东西。)
【讨论】:
@Josh 确实如此,不要被误导。 glibc's implementation。它不是汇编程序,但我确信它已经过高度优化。 @Josh:不是,不要被误导! @Banthar 让我感到惊讶。我已经查看了多个strstr
实现(过去几年我在字符串搜索实现方面进行了大量工作),我看到的每个实现都是简单的搜索——这是一个不错的选择,顺便说一下(见上文)。我也很惊讶他们使用 BM 而不是 Horspool 用于长针,因为众所周知后者在典型情况下平均表现更好(再次,见上文)。
@Konrad:啊,我看到我链接的页面上的算法描述措辞有点混乱。但它确实是一个没有弱点的算法:恒定时间和线性空间。它的存在应该得到更好的了解和赞赏! :) 更重要的是,它应该在哪里:在strstr()
的库实现中。 :)【参考方案2】:
Horspool、KMP 等在最小化字节比较数方面是最佳的。
但是,这不是现代处理器的瓶颈。在 x86/64 处理器上,您的字符串以 cache-line-width 块(通常为 64 字节)的形式加载到 L1 缓存 中。不管你的算法多么聪明,除非它给你带来比这更大的步幅,否则你一无所获;和更复杂的 Horspool 代码(至少一个表查找)无法竞争。
此外,您还被空终止的“C”字符串约束所困扰:代码必须检查每个字节。
strstr()
预计对于各种情况都是最佳的;例如在短字符串中搜索像"\r\n"
这样的小字符串,以及一些更智能的算法可能有希望的更长的字符串。在整个可能的输入范围内,基本的 strchr/memcmp 循环很难被击败。
自 2003 年以来,几乎所有与 x86 兼容的处理器都支持 SSE2。如果您为 glibc 反汇编 strlen()
/x86,您可能已经注意到它使用一些 SSE2 PCMPEQ 和 MOVMASK 操作一次搜索 16 个字节的空终止符。该解决方案非常有效,它击败了明显的超级简单循环,比空字符串更长。
我接受了这个想法,并提出了一个strstr()
,它在所有大于 1 字节的情况下都优于 glibc 的strstr()
--- 相对差异几乎没有实际意义。如果您有兴趣,请查看:
Convergence SSE2 and strstr()
A better strstr()
with no ASM code
如果您希望看到针对超过 15 个字节的目标字符串主导 strstr()
的非 SSE2 解决方案,请查看:
它利用多字节比较而不是strchr()
,来找到执行memcmp 的点。
顺便说一句,您现在可能已经发现,x86 REP SCASB/REP CMPSB 操作对于超过 32 字节的任何内容都会出现问题,并且对于较短的字符串并没有太大的改进。希望英特尔在这方面投入更多的精力,而不是添加 SSE4.2“字符串”操作。
对于足够重要的字符串,我的性能测试表明 BNDM 全面优于 Horspool。 BNDM 更能容忍“病态”情况,例如大量重复模式的最后一个字节的目标。 BNDM 还可以利用 SSE2(128 位寄存器)在效率和启动成本上与 32 位寄存器竞争。源码here.
【讨论】:
您是否尝试过向 glibc 提交补丁?当然,这需要对各种不同的输入进行仔细的基准测试,但您的上述解释似乎是合理的。 所以考虑到缓存优化,试图超越 Horspool 的幼稚搜索是否浪费时间? @Mischa,任何建议如何为 Windows 编译此代码?我不知道什么是 ffs 以及从哪里获得它。 哇,很抱歉我没有回答这个问题。作为记录,MSVC 等价物是 _BitScanForward(),但它有点笨拙。我将其包装为(请原谅 cmets 中的代码,O 编辑器)static inline uint32_t intlowz(uint32_t x) uint32_t pos; return x ? (_BitScanForward(&pos, x), pos) : 32;
【参考方案3】:
没有看到您的代码,很难准确地说出。 strstr
进行了大量优化,通常用汇编语言编写。它执行诸如一次读取 4 个字节的数据并比较它们(如果对齐不正确,则必要时进行位旋转)以最小化内存延迟。它还可以利用 SSE 之类的东西一次加载 16 个字节。如果您的代码一次只加载一个字节,它可能会被内存延迟杀死。
使用您的调试器并逐步完成strstr
的反汇编——您可能会在其中发现一些有趣的东西。
【讨论】:
4 字节比较和 SSE 对字符串搜索的影响非常有限,不幸的是,因为元素之间存在线性依赖关系。特别是,不能只比较不重叠的 4 字节块:即使一次比较 4 个字节,仍然需要比较重叠的块,收益不大。 @Konrad:strstr
基本上可以实现为围绕strchr
和strcmp
/memcmp
的循环(memcmp
需要初始strlen
)。 strchr
可以通过一些位旋转技巧和执行 4 字节加载来实现。 memcmp
可以一次从两个比较数中的每一个加载 4 个字节并比较这些单词。如果两个操作数的对齐方式不同,则一次跟踪两个单词,然后对它们进行位旋转,以使未对齐的单词与另一个字符串进行比较。例如,请参阅 glibc 中的 sysdeps/x86_64/memcmp.S
。
@Konrad:将模式的前 4 个字节与 4 个字节的滑动窗口进行比较,将下一个目标字符串字节移动并插入到 32 位字的底部,您会有所收获。这并不像您希望的那样,因为在同一个寄存器上混合 8 位和 32 位指令会使指令流水线停止。 SSE 则不同:您可以将目标字符串中的 16 个字节与模式的第一个字节进行比较,重复 16 次。这严重摇滚。请参阅下面我的帖子,其中包含更多详细信息。【参考方案4】:
想象一下,你想要清理一些东西。你可以自己清洗,也可以雇十个专业的清洁工来清洗。如果清洁工作是办公楼,则后一种解决方案更可取。如果清洁工作是一个窗户,前者会更好。
您为高效完成工作所花费的时间永远不会得到任何回报,因为这项工作不会花费很长时间。
【讨论】:
以上是关于strstr 比算法快?的主要内容,如果未能解决你的问题,请参考以下文章