最近在做开源项目 php-m3u8 的时候,为了提高解析 m3u8 文件内容的速度,需要实现『字符串是否是另外某个字符串的开头』的需求。不过在 PHP 里实现这个需求可以实现的方法实在是太多了,所以我这里做了一个测试,列出各种实现方式的速度数据,以供参考。
// 首先是用于测试的函数。
function benchmark($callback)
{
$start = microtime(true);
$callback();
return microtime(true) - $start;
}
$test = '#EXT-X-VERSION:3.0'; // 字符串,字符串越长测试效果越好
$patt = 'xx'; // 测试是否是『起始于』,php-m3u8 的 m3u8 内容匹配大部分情况是不能匹配的,所以故意写不包含的情况
$rpatt = sprintf('/^%s/', $patt); // 给正则用的
$len = strlen($patt); // 预先算好长度,后面会用到
$iterCnt = 5555555; // 测试循环的次数
// strstr 使用第三个参数是 true 的情况
echo "strstr-true:\t", benchmark(function() use ($test, $patt, $rpatt, $iterCnt, $len) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r = '' === strstr($test, $patt, true);
}
}), PHP_EOL;
// strstr 第三个参数是 false 的情况
echo "strstr-false:\t", benchmark(function() use ($test, $patt, $rpatt, $iterCnt, $len) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r = $test === strstr($test, $patt);
}
}), PHP_EOL;
// strpos
echo "strpos:\t\t", benchmark(function() use ($test, $patt, $rpatt, $iterCnt, $len) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r = 0 === strpos($test, $patt);
}
}), PHP_EOL;
// strrpos
echo "strrpos:\t", benchmark(function() use ($test, $patt, $rpatt, $iterCnt, $len) {
$len2 = strlen($test);
for ($i = 0; $i < $iterCnt; ++$i) {
$r3 = false !== strrpos($test, $patt, -$len2);
}
}), PHP_EOL;
// substr
echo "substr:\t\t", benchmark(function() use ($test, $patt, $rpatt, $iterCnt, $len) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r1 = substr($test, 0, $len) === $patt;
}
}), PHP_EOL;
// 正则
echo "preg_match:\t", benchmark(function() use ($test, $patt, $rpatt, $iterCnt, $len) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r3 = preg_match($rpatt, $test);
}
}), PHP_EOL;
// strncmp
echo "strncmp:\t", benchmark(function() use ($test, $patt, $iterCnt, $len) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r3 = strncmp($patt, $test, $len);
}
}), PHP_EOL;
// 最后测试一下自己写的函数
function startsWith($str, $prefix)
{
for ($i = 0; $i < strlen($prefix); ++$i) {
if ($prefix[$i] !== $str[$i]) {
return false;
}
}
return true;
}
echo "custom:\t\t", benchmark(function() use ($test, $patt, $iterCnt) {
for ($i = 0; $i < $iterCnt; ++$i) {
$r3 = startsWith($test, $patt);
}
}), PHP_EOL;
测试结果大家可以自行测试,总之结果是令我感到意外的。我觉得这个需求用 strncmp 来做,理论上应该是最快的,结果 strncmp 居然输给了 strpos,而 substr 和 strstr 也没 strpos 快也是挺意外……正则是 PHP 内置函数实现 startsWith 的最后一名,所以从性能考虑如果可以用别的方式代替正则,就尽量别用正则。
最后自己写的函数…… 实在惨不忍睹,所以能用 PHP 内置的函数就请尽量用吧。
所消耗时间排名:strpos ~= strrpos < substr < strstr < strncmp < preg_match < 自己实现的函数。
2017-11-27 补充:如果被测试的字符串长度非常的长,比如 2KB 以上,那么测试的情况又有点不一样了,大家可以修改上面的代码自己在机器上跑一下。总之我这边,如果是最坏情况,也就是被测试字符串根本就不包含测试字符串,strpos 的速度会变得非常的慢,而且慢到连跟自己写的函数都不是一个数量级的(之前我就怀疑过这种情况理论上来说 strpos 会慢,但当时被测试的字符串可能还是不够长,感觉 strpos 表现还是可以……)。所以不管从理论上还是实践得出的结论,在被测试字符串是超长字符串并且在最坏状况下所耗时:strrpos < substr < strncmp < preg_match < custom < (从这开始已经不属于一个数量级)strpos < strstr
当然 strrpos 之所以还那么快肯定也得益于他第三个参数的帮助。
2017-12-15 补充:今天试验了 mb 系列函数(比如 mb_strpos)做了同样的测试,结果真的是非常的惊人……附上 stackoverflow 的一篇问答。
写作累,服务器还越来越贵
求分担,祝愿好人一生平安
天使打赏人