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 基本上可以实现为围绕strchrstrcmp/memcmp 的循环(memcmp 需要初始strlen)。 strchr 可以通过一些位旋转技巧和执行 4 字节加载来实现。 memcmp 可以一次从两个比较数中的每一个加载 4 个字节并比较这些单词。如果两个操作数的对齐方式不同,则一次跟踪两个单词,然后对它们进行位旋转,以使未对齐的单词与另一个字符串进行比较。例如,请参阅 glibc 中的 sysdeps/x86_64/memcmp.S @Konrad:将模式的前 4 个字节与 4 个字节的滑动窗口进行比较,将下一个目标字符串字节移动并插入到 32 位字的底部,您会有所收获。这并不像您希望的那样,因为在同一个寄存器上混合 8 位和 32 位指令会使指令流水线停止。 SSE 则不同:您可以将目标字符串中的 16 个字节与模式的第一个字节进行比较,重复 16 次。这严重摇滚。请参阅下面我的帖子,其中包含更多详细信息。【参考方案4】:

想象一下,你想要清理一些东西。你可以自己清洗,也可以雇十个专业的清洁工来清洗。如果清洁工作是办公楼,则后一种解决方案更可取。如果清洁工作是一个窗户,前者会更好。

您为高效完成工作所花费的时间永远不会得到任何回报,因为这项工作不会花费很长时间。

【讨论】:

以上是关于strstr 比算法快?的主要内容,如果未能解决你的问题,请参考以下文章

Sunday算法

总结:串和数组的学习

寻找素数的三种算法,一个比一个快

28. Implement strStr()

比正则快 M 倍以上!Python 替换字符串的新姿势!

☆打卡算法☆LeetCode 28实现 strStr() 算法解析