
那天晚上,没有事可做。 现在是时候分析一下PHP中数组排序的一些内部方法之间的差异了。
来自master分支的源(现在是7.4,中间是8)
来自PHP 7.3.0的操作码生成器。
在7.3.6上进行测量。
对于书呆子的免责声明:提到几个纳秒和处理器周期,这就是一个名为“ hyperbole”的争论性技巧。
实际上,也许有几十或几百个纳秒和数千个度量,但是它仍然很小,以至于需要保存它们表明代码中有错误。
编译阶段
for ,
foreach ,
do和
while是该语言的关键字,而
array_ *函数是标准库的函数。 因此,首先,ceteris paribus解析器将执行几纳秒的速度。
解析器
在
语句标记之前,所有路径均相同
start -> top_statement_list -> top_statement -> statement
循环在
语句级别定义:
->T_FOR '(' for_exprs ';' for_exprs ';' for_exprs ')' for_statement ->T_FOREACH '(' expr T_AS foreach_variable ')' foreach_statement ->T_FOREACH '(' expr T_AS foreach_variable T_DOUBLE_ARROW foreach_variable ')' foreach_statement ->T_DO statement T_WHILE '(' expr ')' ';' ->T_WHILE '(' expr ')' while_statement
for_exprs和just
expr之间的区别仅在于,对于第一个,允许写入多个
expr ,以逗号分隔。
foreach_variable是一种构造,除了
变量之外 ,还使用
list或
[]跟踪解压缩。
for_statement ,
foreach_statement ,
while_statement与标准
语句的不同之处在于它们增加了解析替代语法的能力:
foreach($array as $element):
函数调用被更深地埋藏:
-> expr -> variable -> callable_variable -> function_call -> name argument_list
callable_variable ,嗯...好笑,不是吗? :)
让我们继续操作码。
例如,让我们简单地对索引数组进行迭代,并打印每个键和值。 显然,为这样的任务使用
for ,
while和
do是没有道理的,但是我们的目标只是显示内部设备。
前言
$arr = ['a', 'b', 'c'];
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 > FE_RESET_R $4 !0, ->7 2 > > FE_FETCH_R ~5 $4, !1, ->7 3 > ASSIGN !2, ~5 4 CONCAT ~7 !2, !1 5 ECHO ~7 6 > JMP ->2 7 > FE_FREE $4 5 8 > RETURN 1
这是怎么回事:
FE_RESET_R :为数组
0创建一个
$ 4迭代器。 或者,如果数组为空,则直接转到指令
7 。
FE_FETCH_R :执行迭代步骤,在
〜5处检索当前键,在
!1处取值。 或者,如果到达数组的末尾,则它前进到指令
7 。
指令
3-6并不是特别有趣。 这里有一个结论,然后返回
FE_FETCH_R 。
FE_FREE :销毁迭代器。
对于...
$length = count($arr); for($i = 0; $i < $length; $i++) echo $i.$arr[$i];
这实际上是
一阵子的特例
$i = 0; while($i < $length) { echo $i.$arr[$i]; $i++; }
这实际上是
if + goto的特例
$i = 0; goto check; body: echo $i.$arr[$i]; $i++; check: if($i < $length) goto body;
这三种情况的操作码几乎相同。 除非使用
if ,
否则JMPNZ将由于进入
if的主体而更改为一对
JMPZ + JMP 。
对于
do循环,由于其验证后的性质,操作码将略有不同。
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 COUNT ~4 !0 2 ASSIGN !1, ~4 6 3 ASSIGN !2, 0 4 > JMP ->10 5 > FETCH_DIM_R ~7 !0, !2 6 CONCAT ~8 !2, ~7 7 ECHO ~8 8 POST_INC ~9 !2 9 FREE ~9 10 > IS_SMALLER ~10 !2, !1 11 > JMPNZ ~10, ->5 12 > > RETURN 1
不出所料,还有更多的操作码。
0-2 :计算
$长度 。
3:$ i = 04、10、11 :对出口状况的初始检查,实际上是对循环主体的出口或过渡的检查。
5(FETCH_DIM_R):$ arr [$ i]6-7 :结论
8-9:$ i ++ (请注意,即使不需要,增量操作码在任何情况下都会返回一个值。因此,需要附加的
FREE指令来清除它)。
10-11 :递增后检查退出条件。
或者你仍然可以迭代
$value = reset($arr); $key = key($arr); while($key !== null) { echo $key.$value; $value = next($arr); $key = key($arr); }
床单,因此扰流板下 line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 5 1 INIT_FCALL 'reset' 2 SEND_REF !0 3 DO_ICALL $4 4 ASSIGN !1, $4 6 5 INIT_FCALL 'key' 6 SEND_VAR !0 7 DO_ICALL $6 8 ASSIGN !2, $6 7 9 > JMP ->20 8 10 > CONCAT ~8 !2, !1 11 ECHO ~8 9 12 INIT_FCALL 'next' 13 SEND_REF !0 14 DO_ICALL $9 15 ASSIGN !1, $9 10 16 INIT_FCALL 'key' 17 SEND_VAR !0 18 DO_ICALL $11 19 ASSIGN !2, $11 7 20 > IS_NOT_IDENTICAL ~13 !2, null 21 > JMPNZ ~13, ->10 11 22 > > RETURN 1
此选项很好,因为它适用于使用任何键迭代数组,而不仅仅是使用单调递增的整数。
reset ,
next和
key函数非常轻巧,但是调用它们仍然有一些开销。 而且,正如我们稍后将看到的,这些成本很高。
尽管此方法与
foreach的原理非常相似,但它们之间有两个根本区别。
1)尽管
reset ,
next和
key (以及
current )直接与数组的内部指针一起工作,但是
foreach使用其自己的迭代器,并且不会更改内部指针的状态。
即
foreach($arr as $v) echo $v.' - '.current($arr).PHP_EOL;
2)当使用
foreach按值进行迭代时,无论对循环内部的数组进行什么操作,原始数据集都会被迭代
foreach($arr as $v) { $arr = ['X','Y','Z']; echo $v.PHP_EOL; } var_dump($arr);
可以在
此RFC中阅读在链接上进行迭代时将发生的情况。 那里的一切都不是那么简单。
具有匿名功能的array_walk
array_walk($arr, function($value, $key){ echo $key.$value;});
由于使用了自定义函数,因此将有一组附加的操作码。
功能介绍
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 0 E > RECV !0 1 RECV !1 2 CONCAT ~2 !1, !0 3 ECHO ~2 4 > RETURN null
主要代号
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 2 0 E > ASSIGN !0, <array> 4 1 INIT_FCALL 'array_walk' 2 SEND_REF !0 3 DECLARE_LAMBDA_FUNCTION '%00%7Bclosure%7D%2Fin%2FHuNEK0x7f9fa62d105a' 4 SEND_VAL ~2 5 DO_ICALL 6 > RETURN 1
由于
array_walk与标准库的其余功能一样,都是固有的,因此在已编译的操作码中没有迭代机制。
INIT_FCALL :初始化对
array_walk的调用
SEND_REF :将数组引用放在调用堆栈上
DECLARE_LAMBDA_FUNCTION :声明一个匿名函数
SEND_VAL :我们在调用堆栈上放置了一个匿名函数
DO_ICALL :运行
array_walk执行
接下来,调用数组的每个元素的lambda会发生魔术。
使用预定义函数的array_walk
与使用匿名方法进行调用没有什么不同,除了可能比在运行时创建lambda的开销少。
line #* EIO op fetch ext return operands --------------------------------------------------------------------------- 3 0 E > ASSIGN !0, <array> 9 2 INIT_FCALL 'array_walk' 3 SEND_REF !0 4 SEND_VAL 'my_echo' 5 DO_ICALL 6 > RETURN 1
结论是司空见惯的。
foreach是为迭代数组量身定制的,而其余的循环只是
if + goto的包装。
标准库的功能按照黑匣子的原理工作。
潜水更深
首先,考虑
for及其操作码
FETCH_DIM_R的情况,该操作码用于通过键提取值。 通过在哈希表(
ZEND_HASH_INDEX_FIND )中进行搜索来提取项目。 在我们的例子中,提取来自
压缩数组 (键是一个连续的数字序列)-这是一个相当容易且快速的操作。 对于未包装的阵列,价格会稍高一些。
现在
foreach (
FE_FETCH_R )。 这里的一切都很陈旧:
- 检查迭代器指针是否在数组之外
- 检索当前键和值
- 增量指针
好吧,像
array_walk这样的函数会发生什么? 尽管它们都做不同的事情,但它们都有一个相同的原理。
如果完全简化,则(伪代码):
reset_pointer() do { value = get_current_value() if (value == NULL) break
实际上,内部的一切都更加复杂,但是本质是相同的-在没有PHP虚拟机参与的情况下(不考虑对用户定义函数的调用),可以快速搜索哈希表。
好吧,一些测量
毕竟,没有测量的物品是什么(从内存中得出的结果相当平均,以至于删除了测量)。
作为一个数组,根据传统,我们将
zend_vm_execute.h用于70.108行。
扰流板计量脚本 <?php $array = explode("\n", file_get_contents('/Users/rjhdby/CLionProjects/php-src/Zend/zend_vm_execute.h')); function myEcho($key, $value){ echo $key . $value; } ob_start(); $startTime = microtime(true); for ($i = 0; $i < 10; $i++) {
每次测量运行10次,选择最频繁出现的前4位数字。
foreach time: 0.12159085273743 foreach(reference) time: 0.11191201210022 for, while time: 0.13130807876587 array_walk(lambda) time: 0.87953400611877 array_walk(static lambda) time: 0.87544417497211 array_walk(name) time: 0.50753092765808 next,key time: 1.06258893013
总结一下
分析结果时,不要忘记考虑它们是在通过7万个元素的数组的10次传递中获得的。
绝对的反英雄是
next / key对
foreach的“模仿”。 除非绝对必要,否则不要这样做。
带着lambda的
array_walk在他的背上呼吸,但有细微差别。 即将到来的准时制可以大大改变这种状况。 也许不变。 将会很有趣。
使用现成的函数进行
array_walk是一个强项。
由于在链接上进行迭代时
foreach的工作方式有所不同(它使用
FE_FETCH_RW操作码而不是
FE_FETCH_R ),因此对其进行了单独的测量。 他的结果确实更快。
事实证明,动态创建lambda并不是最便宜的操作。 似乎只创建了10次。 有必要学习。
所有其他方法均显示出大致相同的结果,但差距很小。
感谢您的关注!
如果您有任何建议,还可以“选择”-在评论中写。 我仍在考虑lambda-这样的性能下降非常奇怪。
UPD为具有静态lambda的
array_walk添加了计量。 区别不明显。