PHP 的 opcode

2024/12/31 PHP

opcodePHP,类似于bytecodeJava的关系,相当于机器码和编译型语言的关系。

PHP是一门解释型语言,它的执行单元就是opcodeZend Engine就是执行opcode的地方,Zend Engine也就是常说的VMJVM比较出名,它就是针对Java设计的VM,这样说应该理解了PHPZend Engineopcode是什么东西了吧。

看看如下代码:

<?php
// opcode1.php

echo "Hello world";

以上代码的opcode是这样的:

0000 ECHO string("Hello world")
0001 RETURN int(1)

如果多个输出呢?

<?php
// opcode2.php

echo "Hello world";
echo "Hello world";
echo "Hello world";

这个问题先暂且不管。

有多种方法可以查看opcode,如Zend Opcache(opcache)扩展、phpdbg接口以及Vulcan Login Dumper(VLD)扩展。

使用 Zend Opcache

前提要求:Zend Opcache扩展必须安装并启用。

opcache.opt_debug_level接收一个十六进制值用于配置opcode的输出,设置为0时会禁用输出。

  • opcache.opt_debug_level=0x10000:输出未优化的opcode
  • opcache.opt_debug_level=0x20000:输出优化后的opcode
  • opcache.opt_debug_level=0x40000:以上下文无关方法形式输出opcode
  • opcache.opt_debug_level=0x200000:以Static Single Assignments形式输出opcode

再次以上述的opcode2.php为例。

$ php -d opcache.enable=On -d opcache.enable_cli=On -d opcache.opt_debug_level=0x10000 opcode2.php

$_main:
     ; (lines=4, args=0, vars=0, tmps=0)
     ; (before optimizer)
     ; /root/opcode.php:1-6
     ; return  [] RANGE[0..0]
0000 ECHO string("Hello world")
0001 ECHO string("Hello world")
0002 ECHO string("Hello world")
0003 RETURN int(1)
$ php -d opcache.enable=On -d opcache.enable_cli=On -d opcache.opt_debug_level=0x20000 opcode2.php

$_main:
     ; (lines=2, args=0, vars=0, tmps=0)
     ; (after optimizer)
     ; /root/opcode.php:1-6
0000 ECHO string("Hello worldHello worldHello world")
0001 RETURN int(1)

这就是对“如果多个输出呢?”这个问题的回答,未经优化时,三个echo语句解析为三条ECHO string("Hello world)指令,优化后合并为一条指令。

使用 phpdbg

$ phpdbg -p opcode2.php

$_main:
     ; (lines=4, args=0, vars=0, tmps=0)
     ; /root/opcode.php:1-6
L0003 0000 ECHO string("Hello world")
L0004 0001 ECHO string("Hello world")
L0005 0002 ECHO string("Hello world")
L0006 0003 RETURN int(1)
[Script ended normally]

VLD是第三方扩展,暂且不表。

通过 opcode 解决疑问

思考以下代码的输出结果:

<?php

$arr = ['a', 'b', 'c'];
$arr2 = $arr;

foreach ($arr as &$item) {
    $item = 'x';
}

foreach ($arr2 as $item) {
    $item = 'y';
}

print_r($arr);
print_r($arr2);

不理解引用的话,可能会觉得很疑惑。我们来看看以上代码的opcode分析一下为什么结果会如下所示。

Array
(
    [0] => x
    [1] => x
    [2] => y
)
Array
(
    [0] => a
    [1] => b
    [2] => c
)

使用opcachephpdbg输出opcode如下,其中使用空行将代码进行了分割便于分析。

0000 ASSIGN CV0($arr) array(...)
0001 ASSIGN CV1($arr2) CV0($arr)

0002 V5 = FE_RESET_RW CV0($arr) 0006
0003 FE_FETCH_RW V5 CV2($item) 0006
0004 ASSIGN CV2($item) string("x")
0005 JMP 0003
0006 FE_FREE V5

0007 V7 = FE_RESET_R CV1($arr2) 0011
0008 FE_FETCH_R V7 CV2($item) 0011
0009 ASSIGN CV2($item) string("y")
0010 JMP 0008
0011 FE_FREE V7

0012 INIT_FCALL 1 96 string("print_r")
0013 SEND_VAR CV0($arr) 1
0014 DO_ICALL
0015 INIT_FCALL 1 96 string("print_r")
0016 SEND_VAR CV1($arr2) 1
0017 DO_ICALL
0018 RETURN int(1)

其中第二和第三部分的opcode正是两段foreach代码,也就是0002-00060007-0011,主要差异如下:

  1. 第一个foreach使用了RE_RESET_RW,对$arr$item可读可写,第二个foreach使用了RE_FETCH_R,对$arr2$item只读;
  2. $item对应的标识符都是CV2,对相当地址的数据进行读写;

通过以上对比,应该可以得到结论,第一个foreach循环时,对$item的修改影响$arr,因此,循环结束后,$arr的值应为:

Array
(
    [0] => x
    [1] => x
    [2] => x
)

与此同时,$item仍然指向$arr[2],第二个foreach循环时,对$item的修改并不会影响$arr2,但由于$item的指向,$arr[2]的值随着$item的改变而改变,最终跟最后一轮循环时的值一致,因此,$arr最终为:

Array
(
    [0] => x
    [1] => x
    [2] => y
)

PHPopcode并没有详细的文档记录,也没有提供向后兼容性,不像Java那样经过精心设计,因此在内部有如“直接运行opcode”之类的RFC都遭到了一致否决,类似的讨论如 https://externals.io/message/111965,还可以在 https://externals.io 查阅更多相关信息。可能关于opcode最权威的资料就在源码中,例如 https://github.com/php/php-src/blob/PHP-8.4.2/Zend/zend_vm_opcodes.h。或许 https://github.com/phpinternalsbook/PHP-Internals-Bookhttps://github.com/tpunt/php-internals-docs 对了解也有一定帮助。

Search

    Table of Contents