为什么mb_strpos()比strpos()慢得多?

时间:2014-06-21 18:16:31

标签: php regex performance strpos

在发现子字符串偏移时,我曾批评preg_match建议===超过preg_match,以避免类型不匹配。

然而,后来答案的作者发现,mb_strpos实际上显着比多字节操作strpos更快。正常mb_strpos比两个函数都快,但当然不能处理多字节字符串。

我了解strpos需要更多而不是strpos。但是,如果正则表达式可以几乎和mb_strpos一样快那么mb_strpos($str, "颜色", 0 ,"GBK"): 15.988190889 (89%) preg_match("/颜色/", $str): 1.022506952 (6%) strpos($str, "dh"): 0.934401989 (5%) 会花多少时间呢?

我怀疑这是一个优化错误。例如,PHP扩展可能比其原生函数慢吗?

$str = "代码dhgd颜色代码";

功能运行10 6 次。绝对时间(s)计算函数的10 6 运行的时间总和,而不是一个函数的平均值。

测试字符串为preg_matchan answer(向下滚动以跳过测试类)。

注意:根据其中一位评论员(以及常识),strpos在比较时也不使用多字节,与{{1}存在相同的错误风险}}

4 个答案:

答案 0 :(得分:17)

要了解函数具有不同运行时的原因,您需要了解它们实际执行的操作。因为将它们总结为'他们在 haystack 中搜索 needle 是不够的。

strpos

如果您查看implementation of strpos,则会使用zend_memstr internally,它会实现漂亮naive algorithm for searching for needle in haystack:基本上,它使用memchr来查找的第一个字节 haystack 中的针,然后使用memcmp检查整个是否从该位置开始。如果没有,它会重复从第一个字节的上一个匹配位置搜索 needle 的第一个字节。

了解这一点,我们可以说 strpos只使用朴素的搜索算法搜索字节序列中的字节序列

mb_strpos

此函数是strpos的多字节对应函数。这使得搜索变得更加复杂,因为您无法在不知道它们属于哪个字符的情况下查看字节。

mb_strpos使用mbfl_strpos,与zend_memstr的简单算法相比,它做得更多,就像200行复杂代码(mbfl_strpos)相比,30光滑的代码行(zend_memstr)。

我们可以跳过part where both needle and haystack are converted to UTF-8 if necessary,然后转到major chunk of code

首先我们有两个设置循环,然后有loop that proceeds the pointer according to the given offset,你可以看到它们知道实际字符以及它们如何跳过整个编码的UTF-8字符:因为UTF-8是一个可变宽度的字符编码,其中每个编码字符的第一个字节表示编码字符的整个长度。此信息存储在u8_tbl数组中。

最后,loop where the actual search happens。在这里我们有一些有趣的东西,因为 haystack 中某个位置的 needle 测试是相反的。如果一个字节不匹配,跳转表jtbl用于在 haystack 中查找 needle 的下一个可能位置。这实际上是Boyer–Moore string search algorithm的实现。

现在我们知道mb_strpos ...

  • 如有必要,将字符串转换为UTF-8
  • 了解实际字符
  • 使用Boyer-Moore搜索算法

preg_match

至于preg_match,它使用PCRE libraryIts standard matching algorithm uses a nondeterministic finite automaton (NFA)找到进行模式树深度优先搜索的匹配项。这基本上是一种天真的搜索方法。

答案 1 :(得分:13)

我要离开preg_match以使分析更加突出。

根据您的观察mb_strposstrpos相比相对较慢,它会让您假设 - 由于消耗的时间 - mb_strpos的作用超过strpos

我认为这种观察是正确的。

然后您询问导致时差的“更多”是什么。

我尝试给出一个简单的答案:“更多”是因为strpos对二进制字符串进行操作(一个字符= 8位= 1个八位字节= 1个字节)。 mb_strpos对编码的字符序列进行操作(几乎所有mb_*函数都可以),可以是X位,甚至可能是每个字符的可变长度。

由于这总是关于特定的字符编码,干草堆和针头串(可能)都需要首先验证该编码,然后找到字符串位置的整个操作需要在特定的字符编码。

这是翻译工作,并且 - 取决于编码 - 还需要特定的搜索算法。

接下来,mb扩展还需要在内存中有一些结构来组织不同的字符编码,无论是转换表和/或特定算法。请参阅您注入的额外参数 - 例如编码的名称。

这比仅进行简单的逐字节比较要多得多。

例如,当您需要对某个字符进行编码或解码时,GBK character encoding非常有趣。在这种情况下,mb字符串函数需要考虑所有这些细节,以确定字符是否以及在哪个位置。由于PHP在用户区中只有二进制字符串,您可以从中调用该函数,因此需要在每个函数调用上完成整个工作。

为了进一步说明这一点,如果您查看支持的编码列表(mb_list_encodings),您还可以找到一些像 BASE64 UUENCODE HTML-ENTITIES Quoted-Printable 。正如您可能想象的那样,所有这些都以不同的方式处理。

例如,单个数字HTML实体最大可达1024字节(如果不是更大)。我知道和喜爱的一个极端例子是this one。但是,对于该编码,它必须由mb_strpos算法处理。

答案 2 :(得分:9)

缓慢的原因

看一下5.5.6 PHP源文件,延迟似乎大部分出现在 mbfilter.c 中,其中 - as hakre surmised - 两者< / em> haystack和needle需要进行验证和转换,每次 mb_strpos(或者,我猜,大多数mb_*系列)都会被调用:

除非haystack采用默认格式,否则将其编码为默认格式

if (haystack->no_encoding != mbfl_no_encoding_utf8) {
        mbfl_string_init(&_haystack_u8);
        haystack_u8 = mbfl_convert_encoding(haystack, &_haystack_u8, mbfl_no_encoding_utf8);
        if (haystack_u8 == NULL) {
                result = -4;
                goto out;
        }
} else {
        haystack_u8 = haystack;
}

除非针是默认格式,否则请将其编码为默认格式

if (needle->no_encoding != mbfl_no_encoding_utf8) {
        mbfl_string_init(&_needle_u8);
        needle_u8 = mbfl_convert_encoding(needle, &_needle_u8, mbfl_no_encoding_utf8);
        if (needle_u8 == NULL) {
                result = -4;
                goto out;
        }
} else {
        needle_u8 = needle;
}

根据valgrind的快速检查,编码转换占mb_strpos运行时的很大一部分,约占总数的84%,或五分之六:

218,552,085  ext/mbstring/libmbfl/mbfl/mbfilter.c:mbfl_strpos [/usr/src/php-5.5.6/sapi/cli/php]
183,812,085  ext/mbstring/libmbfl/mbfl/mbfilter.c:mbfl_convert_encoding [/usr/src/php-5.5.6/sapi/cli/php]

似乎与OP mb_strposstrpos的时间安排一致。

未考虑编码,mb_strpos&#39}字符串与strpos 略长字符串完全相同。好吧,如果你有非常笨拙的字符串,那么字符串最多可以是的四倍,但即使这样,你也会得到一个四倍的延迟,而不是二十倍。额外的5-6倍减速是由编码时间引起的。

加速mb_strpos ...

那你能做什么?您可以通过确保内部已经包含&#34; basic&#34;中的字符串来跳过这两个步骤。 mbfl*执行转换和比较的格式,mbfl_no_encoding_utf8(UTF-8):

  • 将您的数据保存为UTF-8。
  • 尽快将用户输入转换为UTF-8。
  • 如有必要,转换回客户端编码(如果需要)。

然后你的伪代码:

$haystack = "...";
$needle   = "...";

$res = mb_strpos($haystack, $needle, 0, $Encoding);

变为:

$haystack = "...";
$needle   = "...";

mb_internal_encoding('UTF-8') or die("Cannot set encoding");
$haystack   = mb_convert_encoding($haystack, 'UTF-8' [, $SourceEncoding]);
$needle     = mb_convert_encoding($needle, 'UTF-8', [, $SourceEncoding]);

$res = mb_strpos($haystack, $needle, 0);

......值得的时候

当然,如果&#34;设置时间&#34;并且整个UTF-8基地的维护明显小于&#34;运行时间&#34;在每个mb_*函数中隐式进行转换。

答案 3 :(得分:0)

mb_性能问题可能是由混乱的php-mbstring软件包安装引起的(在linux上)。明确地为PHP安装的确切版本安装它帮助了我。

sudo apt-get install php7.1-mbstring

...

Before: Time: 16.17 seconds, Memory: 36.00MB OK (3093 tests, 40272 assertions)
After:  Time:  1.81 seconds, Memory: 36.00MB OK (3093 tests, 40272 assertions)