问题
在PHP中,有多种字符串拼接的方式可供选择,共有:
1
|
. , .= , sprintf, vprintf, join, implode
|
那么,那种才是最快的,或者那种才是最适合业务使用的,需要进一步探究。
用到的工具
PHP7.1.16 PHP5.4 VLD XDebug phpunit4 以及自己写的一个Benchmark工具。
PHP54环境
PHPUnit测试结果
使用以下代码,分别测试了上面的几种字符串拼接方式(拼接方式无法对变量赋值,故用处不大,没有测,join和implode是相等的,仅仅测试了其中一个)
测试条件:2C-4T 8G 拼接5W次,重复10次取平均值。
测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
<?php /** * Created by PhpStorm. * User: shixi_qingzhe * Date: 18/5/18 * Time: 下午9:31 * 几种字符串拼接的测试,表面测试 * * * 最快字符串拼接方式 . , .= , sprintf , join, implode, concat */ require_once __DIR__ . '/lib.php';
class ConcatTest extends \PHPUnit_Framework_TestCase { private $Count = 50000;
private $reTryTime = 10;
private $tempStr = '19826318654817aasdasdadasd';
public function testDotEqual() { $timer = Benchmark::getInstance(); $timeAVG = $timer->tryManyTimes(function () { $result = ""; for ($i = 0; $i < $this -> Count; $i++) { $result .= $this->tempStr; } }, $this -> reTryTime); echo "time By .= is : ".$timeAVG."\n"; }
// public function testDouhao() // { // $timer = Benchmark::getInstance(); // $timeAvg = $timer -> tryManyTimes(function () { // $result = ""; // for ($i = 0; $i < $this -> Count;$i++) { // $result = $result.$this -> tempStr; // } // }, $this -> reTryTime); // echo "time By , is : ".$timeAvg."\n"; // }
public function testDot() { $timer = Benchmark::getInstance(); $timeAvg = $timer -> tryManyTimes(function () { $result = ""; for ($i = 0; $i < $this -> Count;$i++) { $result = $result.$this -> tempStr; } }, $this -> reTryTime); echo "time By . is : ".$timeAvg."\n"; }
public function testSprintf() { $timer = Benchmark::getInstance(); $timeAvg = $timer -> tryManyTimes(function() { $result = ""; for ($i = 0;$i < $this -> Count;$i++) { $result = sprintf("%s%s", $result, $this -> tempStr); } }, $this -> reTryTime); echo "time By sprintf() is : ".$timeAvg."\n"; }
public function testVsprintf() { $timer = Benchmark::getInstance(); $timeAvg = $timer -> tryManyTimes(function() { $arg = []; for ($i = 0;$i < $this -> Count;$i++) { $arg[] = $this -> tempStr; } $result = vsprintf("%s", $arg); }, $this -> reTryTime); echo "time By vsprintf() is : ".$timeAvg."\n"; }
public function testJoin() { $timer = Benchmark::getInstance(); $timeAvg = $timer -> tryManyTimes(function () { // alloc the array $resultArr = []; for ($i = 0;$i < $this -> Count;$i++) { $resultArr[] = $this -> tempStr; } $result = implode('',$resultArr); }, $this -> reTryTime); echo "time by Join() / Implode() is : ".$timeAvg."\n"; } }
|
测试结果为:
1 2 3 4 5 6 7 8 9 10 11 12
|
bogon:a_compare root# /usr/local/Cellar/php54/bin/php /usr/local/Cellar/phpunit4 ConcatTest.php PHPUnit 4.0.20 by Sebastian Bergmann.
.time By .= is : 0.0050616264343262 .time By . is : 6.156693148613 .time By sprintf() is : 8.7994904279709 .time By vsprintf() is : 0.014266705513 .time by Join() / Implode() is : 0.0092714786529541
Time: 2.49 minutes, Memory: 13.00Mb
|
可以看出,执行速度最快的方法是 “.=” 方法,其次是给出数组参数并将其粘和的Vsprintf、以及Implode。
性能最差的是“.”方法。
因此可以得出一个结论,在PHP54的条件下,使用“.=”的方法来拼接字符串,效率是最高的。
对于implode sprintf等方法可以深入PHP的源码查看一下。对于“.=”等运算符,可以使用VLD打印出执行过程中的OPcode,来解释相关原因。
Sprintf方法源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
/private/var/root/Downloads/php-5.4.45/main/php_sprintf.c // php54 和 php7本方法代码相同 PHPAPI int php_sprintf (char*s, const char* format, ...) { va_list args; int ret;
va_start (args, format); s[0] = '\0'; ret = vsprintf (s, format, args); va_end (args); return (ret < 0) ? -1 : ret; }
|
可以看出,实际实现是通过C语言库中的stdarg.h中的va_list配合va_start实现参数个数不定,并且将参数化为数组,然后调用C语言本身具有的vsprintf(format, argArr)进行拼接。
可以从以上phpunit执行结果中获得与vsprintf的对比,可以得知 绝大部分性能消耗在va_start O(n)。具体va_start为什么会消耗性能还是有待考察的。
Implode方法源码分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
|
/private/var/root/Downloads/php-5.4.45/ext/standard/string.c
PHP5 Version 时间复杂度为O(n2) = memcpy( O(n) ) * zend_hash_get_current_data_ex(O(n))
PHPAPI void php_implode(zval *delim, zval *arr, zval *return_value TSRMLS_DC) { zval **tmp; HashPosition pos; smart_str implstr = {0}; int numelems, i = 0; zval tmp_val; int str_len;
numelems = zend_hash_num_elements(Z_ARRVAL_P(arr));
if (numelems == 0) { RETURN_EMPTY_STRING(); }
zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(arr), &pos);
while (zend_hash_get_current_data_ex(Z_ARRVAL_P(arr), (void **) &tmp, &pos) == SUCCESS) { switch ((*tmp)->type) { case IS_STRING: smart_str_appendl(&implstr, Z_STRVAL_PP(tmp), Z_STRLEN_PP(tmp)); break;
case IS_LONG: { char stmp[MAX_LENGTH_OF_LONG + 1]; str_len = slprintf(stmp, sizeof(stmp), "%ld", Z_LVAL_PP(tmp)); smart_str_appendl(&implstr, stmp, str_len); } break;
case IS_BOOL: if (Z_LVAL_PP(tmp) == 1) { smart_str_appendl(&implstr, "1", sizeof("1")-1); } break;
case IS_NULL: break;
case IS_DOUBLE: { char *stmp; str_len = spprintf(&stmp, 0, "%.*G", (int) EG(precision), Z_DVAL_PP(tmp)); smart_str_appendl(&implstr, stmp, str_len); efree(stmp); } break;
case IS_OBJECT: { int copy; zval expr; zend_make_printable_zval(*tmp, &expr, ©); smart_str_appendl(&implstr, Z_STRVAL(expr), Z_STRLEN(expr)); if (copy) { zval_dtor(&expr); } } break;
default: tmp_val = **tmp; zval_copy_ctor(&tmp_val); convert_to_string(&tmp_val); smart_str_appendl(&implstr, Z_STRVAL(tmp_val), Z_STRLEN(tmp_val)); zval_dtor(&tmp_val); break;
}
if (++i != numelems) { smart_str_appendl(&implstr, Z_STRVAL_P(delim), Z_STRLEN_P(delim)); } zend_hash_move_forward_ex(Z_ARRVAL_P(arr), &pos); } smart_str_0(&implstr);
if (implstr.len) { RETURN_STRINGL(implstr.c, implstr.len, 0); } else { smart_str_free(&implstr); RETURN_EMPTY_STRING(); } }
|
其实,认为直接从代码层面分析还是不够直观,或者比较菜导致看不懂源码,此时可以通过VLD扩展来分析每个步骤执行的时候都有哪些操作指令被执行,这样能够更加直观的看到性能差异。
“.=”的VLD分析
相关差异测试VLD代码已经上传到GitHub:
测试参数:php -dvld.active=1 XXX.php
“.”的VLD分析
“,”的VLD分析
“Join”的VLD分析
“Sprintf Vsprintf”的VLD分析
可以看出,语法结构层面比函数调用的操作数要少,因此,如果在业务中,如果能用语法结构的字符串拼接尽量使用语法结构。
可以看出,“.=”比“.”的操作数中,使用了ASSIGN_CONCAT 代替了“.”使用的ASSIGN + CONCAT的两个操作,使得消耗的时间更少。
还可以看出,如果在直接echo并且结束程序的情况下,“,”的性能最佳,其只使用了几个性能较好的ECHO操作就完成了。
PHP7 环境
测试条件,测试代码均相同,测试结果如下:
1 2 3 4 5 6 7 8
|
bogon:ConcatDeepCompare root# /usr/local/Cellar/phpunit4 ConcatTest.php PHPUnit 4.0.20 by Sebastian Bergmann.
.time By .= is : 0.0073251962661743 .time By . is : 1.7567269802094 .time By sprintf() is : 1.8133352994919 .time By vsprintf() is : 0.0089122295379639 .time by Join() / Implode() is : 0.0079930782318115
|
对比以上PHP54的结果,可以发现PHP7的测试结果平均时长比PHP54短了5倍左右,得益于PHP7对Zval的优化,使得COW等耗费内存的现象得到缩减,进而性能提升。
相关鸟哥博客:
http://www.laruence.com/2018/04/08/3170.html Zval
http://www.laruence.com/2018/04/08/3179.html Reference
VLD结果
“.”
“,”
“.=”
“Join”
“Sprintf”
“Vsprintf”
总结
-
“.=”在可用性以及性能是最佳的
-
“,” 在仅仅echo 输出的情况下性能最优
-
“.” 在可用的情况下性能最差
-
“Join/Vsprintf”性能较优