PHP 7的多字节(mb_)函数比5.3版本慢了大约60%(仅限于Windows系统)。

8

我的应用程序广泛使用 mb_ 字符串函数,升级到 php 7 后导致应用程序整体变慢。我把问题追踪到了 mb_ 字符串函数上。以下是基准测试代码和结果:

$time = microtime();
$time = explode(' ', $time);
$start = $time[1] + $time[0];
$startms = $time[0];
    for ($i=0; $i<100000; $i++) {
        $a = mb_strlen("fdsfdssdfoifjosdifjosdifjosdij:ά", "UTF-8");
    }
$time = microtime();
$time = explode(' ', $time);
$finish = $time[1] + $time[0];
$finishms = $time[0];
$total_time = round(($finish - $start), 4);
echo "mb_strlen: " . $total_time*1000 ." milliseconds<br/>";

$time = microtime();
$time = explode(' ', $time);
$start = $time[1] + $time[0];
$startms = $time[0];
    for ($i=0; $i<100000; $i++) {
        $a = mb_stripos("fdsfdssdfoifjosdifjosdifjosdij:ά", "α", 0, "UTF-8");
    }
$time = microtime();
$time = explode(' ', $time);
$finish = $time[1] + $time[0];
$finishms = $time[0];
$total_time = round(($finish - $start), 4);
echo "mb_stripos: " . $total_time*1000 ." milliseconds<br/>";


$time = microtime();
$time = explode(' ', $time);
$start = $time[1] + $time[0];
$startms = $time[0];
    for ($i=0; $i<100000; $i++) {
        $a = mb_substr("fdsfdssdfoifjosdifjosdifjosdij:ά", $i, 1, "UTF-8");
    }
$time = microtime();
$time = explode(' ', $time);
$finish = $time[1] + $time[0];
$finishms = $time[0];
$total_time = round(($finish - $start), 4);
echo "mb_substr: " . $total_time*1000 ." milliseconds<br/>";

该平台为Windows 7 64位系统,使用IIS 7.5:

php 5.3.28
mb_strlen: 250 milliseconds
mb_stripos: 3078.1 milliseconds
mb_substr: 281.3 milliseconds

php 7.1.1
mb_strlen: 406.3 milliseconds
mb_stripos: 4796.9 milliseconds
mb_substr: 421.9 milliseconds

我不知道我的设置是否有误,但多字节函数变慢似乎难以想象。有什么想法和解决方法吗?非常感谢。
编辑:正如apokryfos的评论所建议的那样,这可能只是Windows的问题。

@apokryfos,我不知道你提供的测试链接所运行的操作系统是什么,也许这是与 PHP 的 Windows 版本有关的问题。 - MirrorMirror
2
仅供阅读:microtime接受一个布尔参数,使其返回一个浮点数 - 不需要使用explode等函数。 - 想一想:这可能是整个问题的关键所在,$time = explode(' ', $time); $start = $time[1] + $time[0];代表什么?你只是将当前时间戳的毫秒部分加到秒部分上吗? - ccKep
1
我明白了,我误以为它在那种情况下返回“66539800 1499759652”- 是我的错。关于可读性的观点仍然成立;-) - ccKep
1
为什么这是“难以想象”的呢?很可能是这个扩展名在这些年里发生了变化。 - user3942918
1
@PaulCrovella,你说得对。然而,正如apokryfos通过他的链接所建议的那样,我所描述的情况似乎在Linux下不会发生,这可能意味着库在Windows实现中存在问题。 - MirrorMirror
显示剩余5条评论
2个回答

4
这似乎是一个“性能回归”错误。应该提交一个错误报告,以便php核心开发人员可以查看它,在bugs.php.net上提交报告。
同时,我注意到在您的代码片段中,您只使用UTF-8编码。只要您只使用UTF-8编码,您可能可以通过使用preg_来加速它,因为它仅支持一种Unicode字符集:UTF-8。以下是我的尝试:
function _mb_strlen(string $str, string $encoding = 'UTF-8'): int {
    assert ( $encoding === 'UTF-8' );
    preg_match ( '/.$/u', $str, $matches, PREG_OFFSET_CAPTURE );
    return empty ( $matches ) ? 0 : ($matches [0] [1]) + 1;
}
function _mb_stripos(string $haystack, string $needle, int $offset = 0, string $encoding = 'UTF-8') {
    assert ( $encoding === 'UTF-8' );
    if ($offset !== 0) {
        throw new LogicException ( 'NOT IMPLEMENTED' );
    }
    preg_match ( '/' . preg_quote ( $needle ) . '/ui', $haystack, $matches, PREG_OFFSET_CAPTURE );
    return empty ( $matches ) ? false : $matches [0] [1];
}
function _mb_substr(string $str, int $start, int $length = NULL, string $encoding = 'UTF-8'): string {
    assert ( $encoding === 'UTF-8' );
    if ($start < 0) {
        throw new LogicException ( 'NOT IMPLEMENTED' );
    } elseif ($start > 0) {
        $rex = '/.{' . $start . '}(.{0,';
    } else {
        $rex = '/(.{0,';
    }
    if ($length !== NULL) {
        $rex .= $length;
    }
    $rex .= '})/u';
    preg_match ( $rex, $str, $matches );
    // var_dump ( $rex, $matches );
    return empty ( $matches ) ? '' : $matches [1];
}

以下是我在Debian 9 Linux(内核4.9)上使用PHP 7.0进行的10万次迭代测试结果:

mb_strlen变慢了,从约60毫秒变为100毫秒。

mb_stripos速度大大提高,从约1400毫秒降至75毫秒。

mb_substr变得非常缓慢,从约47毫秒变为约800毫秒。

  • 但我建议您在Windows上重新运行这些测试,因为您认为这可能是Windows独有的问题。

请注意,这些函数并不完整,因为它们会抛出LogicException异常。

此外,请注意由于preg_的限制,我必须将mb_substr的迭代次数限制在65000次以内。

for($i = 0; $i < 65000; $i ++) {
    $a = mb_substr ( "fdsfdssdfoifjosdifjosdifjosdij:ά", $i, 1, "UTF-8" );
}

因为,如果您要求preg查找一个长度超过65,000个字符的字符串,它将会出现错误... 另外请注意,您的基准代码可以简化很多,所有这些。
$time = microtime();
$time = explode(' ', $time);
$start = $time[1] + $time[0];
$startms = $time[0];
    for ($i=0; $i<100000; $i++) {
        $a = mb_strlen("fdsfdssdfoifjosdifjosdifjosdij:ά", "UTF-8");
    }
$time = microtime();
$time = explode(' ', $time);
$finish = $time[1] + $time[0];
$finishms = $time[0];
$total_time = round(($finish - $start), 4);
echo "mb_strlen: " . $total_time*1000 ." milliseconds<br/>";

可以简单地替换为

$starttime=microtime(true);
    for ($i=0; $i<100000; $i++) {
        $a = mb_strlen("fdsfdssdfoifjosdifjosdifjosdij:ά", "UTF-8");
    }
$endtime=microtime(true);
echo "mb_strlen: " . number_format(($endtime-$starttime),3) ." seconds<br/>";

输出结果如下:mb_strlen: 0.085秒(大约85毫秒)

或者

echo "mb_strlen: " . number_format(($endtime - $starttime) * 1000),2) . " milliseconds<br/>";

(我可以猜测这可能与realloc()的性能有关,其中Linux比Windows更占优势,但我没有证据。)

谢谢您的回复。关于时间测量优化代码的评论:它之所以是现在这个样子,而不像您和其他人建议的那样进行优化,是因为当t>1秒时会出现问题(显示负值等)。 - MirrorMirror
1
哦,要解决这个问题,你可以使用number_format()函数 :) (我现在在手机上,所以我不会修复它,但当我回到电脑前,我会修复的) - hanshenrik
1
@MIrrorMirror用number_format修复了它^^(如果您不想使用number_format的其他格式化操作,只需在末尾给它2个空字符串参数即可)。 - hanshenrik

4
我可以确认你在Windows 7上的结果是可重现的。 经过一些实验,我发现了一个快速的解决方案,我认为它甚至不应该有任何影响。
正如你从mb_strlen()函数签名中看到的那样, 如果省略编码参数,它将使用内部编码。 这也适用于你使用的其他函数。
mixed mb_strlen ( string $str [, string $encoding = mb_internal_encoding() ] )

我发现奇怪的是,如果你通过调用mb_internal_encoding("UTF-8")设置内部编码为UTF-8并省略编码参数,函数会变得更快。

PHP 5.5 结果:

5.5.12

with encoding parameter:
- mb_strlen: 172 ms, result: 5
- mb_substr: 218 ms, result: 
- mb_strpos: 218 ms, result: 3
- mb_stripos: 1,669 ms, result: 3
- mb_strrpos: 234 ms, result: 3
- mb_strripos: 1,685 ms, result: 3

with internal encoding:
- mb_strlen: 47 ms, result: 5
- mb_substr: 78 ms, result: 
- mb_strpos: 62 ms, result: 3
- mb_stripos: 1,669 ms, result: 3
- mb_strrpos: 94 ms, result: 3
- mb_strripos: 1,669 ms, result: 3

PHP 7.0的结果:

7.0.12

with encoding parameter:
- mb_strlen: 640 ms, result: 5
- mb_substr: 702 ms, result: 
- mb_strpos: 686 ms, result: 3
- mb_stripos: 7,067 ms, result: 3
- mb_strrpos: 749 ms, result: 3
- mb_strripos: 7,130 ms, result: 3

with internal encoding:
- mb_strlen: 31 ms, result: 5
- mb_substr: 31 ms, result: 
- mb_strpos: 47 ms, result: 3
- mb_stripos: 7,270 ms, result: 3
- mb_strrpos: 62 ms, result: 3
- mb_strripos: 7,116 ms, result: 3

不幸的是,这种快速解决方案并不完美,因为mb_stripos()mb_strripos()似乎没有受到影响。它们仍然很慢。

以下是缩短后的代码:

echo PHP_VERSION."\n";
echo "\nwith encoding parameter:\n";

$t = microtime(true)*1000;
for($i=0; $i<100000; $i++){
    $n = mb_strlen("あえいおう","UTF-8");
}
$t = microtime(true)*1000-$t;
echo "- mb_strlen: ".number_format($t)." ms, result: {$n}\n";

$t = microtime(true)*1000;
for($i=0; $i<100000; $i++){
    $n = mb_substr("あえいおう",-1,1,"UTF-8");
}
$t = microtime(true)*1000-$t;
echo "- mb_substr: ".number_format($t)." ms, result: {$n}\n";

//set internal encoding
//and omit encoding parameter

mb_internal_encoding("UTF-8");
echo "\nwith internal encoding:\n";

$t = microtime(true)*1000;
for($i=0; $i<100000; $i++){
    $n = mb_strlen("あえいおう");
}
$t = microtime(true)*1000-$t;
echo "- mb_strlen: ".number_format($t)." ms, result: {$n}\n";

$t = microtime(true)*1000;
for($i=0; $i<100000; $i++){
    $n = mb_substr("あえいおう",-1,1);
}
$t = microtime(true)*1000-$t;
echo "- mb_substr: ".number_format($t)." ms, result: {$n}\n";

哇,那很奇怪。 - hanshenrik
有人请提出一个错误报告,这一定是个错误。 - hanshenrik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接