- 浏览: 29290 次
- 性别:
- 来自: 沈阳
文章分类
最新评论
从PHP语法糖剖析Zend VM引擎
## 1.
先说个PHP5.3+ 的语法糖,通常我们这样写:
<?php
$a = 0;
$b = $a ? $a : 1;
语法糖可以这样写:
<?php
$a = 0;
$b = $a ?: 1;
执行结果$b = 1,后面写法更简洁,但通常不太建议用太多语法糖,特别是容易理解混淆的,比如PHP 7 新增加??如下:
<?php
$b = $a ?? 1;
相当于:
<?php
$b = isset($a) ? $a : 1;
?: 和 ?? 你是不是容易搞混,如果这样,我建议宁可不用,代码可读性强,易维护更重要。
语法糖不是本文的重点,我们的目的是从语法糖入手聊聊Zend VM的解析原理。
## 2.
分析的PHP源码分支 => remotes/origin/PHP-5.6.14,关于如何通过vld查看opcode,请看我之前写的这篇文章:
<http://www.yinqisen.cn/blog-680.html>
<?php
$a = 0;
$b = $a ?: 1;
对应的opcdoe如下:
number of ops: 5
compiled vars: !0 = $a, !1 = $b
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 0
3 1 JMP_SET_VAR $1 !0
2 QM_ASSIGN_VAR $1 1
3 ASSIGN !1, $1
4 4 > RETURN 1
branch: # 0; line: 2- 4; sop: 0; eop: 4; out1: -2
path #1: 0,
vim Zend/zend_language_parser.y +834
~~~.bash
834 › |› expr '?' ':' { zend_do_jmp_set(&$1, &$2, &$3 TSRMLS_CC); }
835 › › expr { zend_do_jmp_set_else(&$$, &$5, &$2, &$3 TSRMLS_CC); }
~~~
如果你喜欢,可以自己动手,重新定义 ?: 的语法糖。遵循BNF文法规则,使用bison解析,有兴趣可以自行Google相关知识,继续深入了解。
从vld的opcode可以知道,执行了 zend_do_jmp_set_else,代码在 Zend/zend_compile.c 中:
~~~.java
void zend_do_jmp_set_else(znode *result, const znode *false_value, const znode *jmp_token, const znode *colon_token TSRMLS_DC)
{
› zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
› SET_NODE(opline->result, colon_token);
› if (colon_token->op_type == IS_TMP_VAR) {
› › if (false_value->op_type == IS_VAR || false_value->op_type == IS_CV) {
› › › CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].opcode = ZEND_JMP_SET_VAR;
› › › CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].result_type = IS_VAR;
› › › opline->opcode = ZEND_QM_ASSIGN_VAR;
› › › opline->result_type = IS_VAR;
› › } else {
› › › opline->opcode = ZEND_QM_ASSIGN;
› › }
› } else {
› › opline->opcode = ZEND_QM_ASSIGN_VAR;
› }
› opline->extended_value = 0;
› SET_NODE(opline->op1, false_value);
› SET_UNUSED(opline->op2);
› GET_NODE(result, opline->result);
› CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].op2.opline_num = get_next_op_number(CG(active_op_array));
› DEC_BPC(CG(active_op_array));
}
~~~
## 3.
重点两个opcode,ZEND_JMP_SET_VAR 和 ZEND_QM_ASSIGN_VAR,怎么接着读代码呢?下面说下PHP的opcode。
PHP5.6有167个opcode,意味着可以执行167种不同的计算操作,官方文档看这里<http://php.net/manual/en/internals2.opcodes.list.php>
PHP内部使用_zend_op 这个结构体来表示opcode, vim Zend/zend_compile.h +111
111 struct _zend_op {
112 › opcode_handler_t handler;
113 › znode_op op1;
114 › znode_op op2;
115 › znode_op result;
116 › ulong extended_value;
117 › uint lineno;
118 › zend_uchar opcode;
119 › zend_uchar op1_type;
120 › zend_uchar op2_type;
121 › zend_uchar result_type;
122 }
PHP 7.0略有不同,主要区别在针对64位系统 uint换成uint32_t,明确指定字节数。
你把opcode当成一个计算器,只接受两个操作数(op1, op2),执行一个操作(handler, 比如加减乘除),然后它返回一个结果(result)给你,再稍加处理算术溢出的情况(extended_value)。
Zend的VM对每个opcode的工作方式完全相同,都有一个handler(函数指针),指向处理函数的地址。这是一个C函数,包含了执行opcode对应的代码,使用op1,op2做为参数,执行完成后,会返回一个结果(result),有时也会附加一段信息(extended_value)。
用我们例子中的操作数 ZEND_JMP_SET_VAR 说明,vim Zend/zend_vm_def.h +4995
4942 ZEND_VM_HANDLER(158, ZEND_JMP_SET_VAR, CONST|TMP|VAR|CV, ANY)
4943 {
4944 › USE_OPLINE
4945 › zend_free_op free_op1;
4946 › zval *value, *ret;
4947
4948 › SAVE_OPLINE();
4949 › value = GET_OP1_ZVAL_PTR(BP_VAR_R);
4950
4951 › if (i_zend_is_true(value)) {
4952 › › if (OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) {
4953 › › › Z_ADDREF_P(value);
4954 › › › EX_T(opline->result.var).var.ptr = value;
4955 › › › EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
4956 › › } else {
4957 › › › ALLOC_ZVAL(ret);
4958 › › › INIT_PZVAL_COPY(ret, value);
4959 › › › EX_T(opline->result.var).var.ptr = ret;
4960 › › › EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
4961 › › › if (!IS_OP1_TMP_FREE()) {
4962 › › › › zval_copy_ctor(EX_T(opline->result.var).var.ptr);
4963 › › › }
4964 › › }
4965 › › FREE_OP1_IF_VAR();
4966 #if DEBUG_ZEND>=2
4967 › › printf("Conditional jmp to %d\n", opline->op2.opline_num);
4968 #endif
4969 › › ZEND_VM_JMP(opline->op2.jmp_addr);
4970 › }
4971
4972 › FREE_OP1();
4973 › CHECK_EXCEPTION();
4974 › ZEND_VM_NEXT_OPCODE();
4975 }
i_zend_is_true 来判断操作数是否为true,所以ZEND_JMP_SET_VAR是一种条件赋值,相信大家都能看明白,下面讲重点。
注意`zend_vm_def.h`这并不是一个可以直接编译的C的头文件,只能说是一个模板,具体可编译的头为`zend_vm_execute.h`(这个文件可有45000多行哦),它并非手动生成,而是由`zend_vm_gen.php`这个PHP脚本解析`zend_vm_def.h`后生成(有意思吧,先有鸡还是先有蛋,没有PHP 哪来的这个脚本?),猜测这个是后期产物,早期php版本应该不会用这个。
上面ZEND_JMP_SET_VAR的代码,根据不同参数 `CONST|TMP|VAR|CV` 最终会生成不同类型的,但功能一致的handler函数:
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
这么做的目的是为了在编译期确定handler,提升运行期的性能。不这么做,在运行期根据参数类型选择,也可以做到,但性能不好。当然这么做有时也会生成一些垃圾代码(看似无用),不用担心,C的编译器会进一步优化处理。
zend_vm_gen.php 也可以接受一些参数,细节在PHP源码中的README文件 `Zend/README.ZEND_VM` 有详细说明。
## 4.
讲到这里,我们知道opcode怎么和handler对应了。但是在整体上还有一个过程,就是语法解析,解析后所有的opcode是怎么串联起来的呢?
语法解析的细节就不说了,解析过后,会有个包含所有opcode的大数组(说链表可能更准确),从上面代码我们可以看到,每个handler执行完后,都会调用 ZEND_VM_NEXT_OPCODE(),取出下一个opcode,继续执行,直到最后退出,循环的代码 vim Zend/zend_vm_execute.h +337:
~~~.java
ZEND_API void execute_ex(zend_execute_data *execute_data TSRMLS_DC)
{
› DCL_OPLINE
› zend_bool original_in_execution;
› original_in_execution = EG(in_execution);
› EG(in_execution) = 1;
› if (0) {
zend_vm_enter:
› › execute_data = i_create_execute_data_from_op_array(EG(active_op_array), 1 TSRMLS_CC);
› }
› LOAD_REGS();
› LOAD_OPLINE();
› while (1) {
› int ret;
#ifdef ZEND_WIN32
› › if (EG(timed_out)) {
› › › zend_timeout(0);
› › }
#endif
› › if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
› › › switch (ret) {
› › › › case 1:
› › › › › EG(in_execution) = original_in_execution;
› › › › › return;
› › › › case 2:
› › › › › goto zend_vm_enter;
› › › › › break;
› › › › case 3:
› › › › › execute_data = EG(current_execute_data);
› › › › › break;
› › › › default:
› › › › › break;
› › › }
› › }
› }
› zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen");
}
~~~
宏定义, vim Zend/zend_execute.c +1772
1772 #define ZEND_VM_NEXT_OPCODE() \
1773 › CHECK_SYMBOL_TABLES() \
1774 › ZEND_VM_INC_OPCODE(); \
1775 › ZEND_VM_CONTINUE()
329 #define ZEND_VM_CONTINUE() return 0
330 #define ZEND_VM_RETURN() return 1
331 #define ZEND_VM_ENTER() return 2
332 #define ZEND_VM_LEAVE() return 3
while是一个死循环,执行一个handler函数,除个别情况,多数handler函数末尾都调用ZEND_VM_NEXT_OPCODE() -> ZEND_VM_CONTINUE(),return 0,继续循环。
> 注:比如 yield 协程是个例外,它会返回1,直接return出循环。以后有机会我们再单独对yield做分析。
希望你看完上面内容,对PHP Zend 引擎的解析过程有个详细的了解,下面我们基于原理的分析,再简单聊聊PHP的优化。
## 5. PHP优化注意事项
### 5.1 echo 输出
<?php
$foo = 'foo';
$bar = 'bar';
echo $foo . $bar;
vld 查看opcode:
number of ops: 5
compiled vars: !0 = $foo, !1 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'foo'
3 1 ASSIGN !1, 'bar'
4 2 CONCAT ~2 !0, !1
3 ECHO ~2
5 4 > RETURN 1
branch: # 0; line: 2- 5; sop: 0; eop: 4; out1: -2
path #1: 0,
ZEND_CONCAT 连接 $a和$b的值,保存到临时变量~2中,然后echo 出来。这个过程中涉及要分配一块内存,用于临时变量,用完后还要释放,还需要调用拼接函数,执行拼接过程。
如果换成这样写:
<?php
$foo = 'foo';
$bar = 'bar';
echo $foo, $bar;
对应的opcode:
number of ops: 5
compiled vars: !0 = $foo, !1 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'foo'
3 1 ASSIGN !1, 'bar'
4 2 ECHO !0
3 ECHO !1
5 4 > RETURN 1
branch: # 0; line: 2- 5; sop: 0; eop: 4; out1: -2
path #1: 0,
不需要分配内存,也不需要执行拼接函数,是不是效率更好呢!想了解拼接过程,可以根据本文讲的内容,自行查找 ZEND_CONCAT 这个opcode对应的handler,做了好多事情哦。
### 5.2 define()和const
const关键字是从5.3开始引入的,和define有很大差别,和C语言的`#define`倒是含义差不多。
* define() 是函数调用,有函数调用开销。
* const 是关键字,直接生成opcode,属于编译期能确定的,不需要动态在执行期分配。
const 的值是死的,运行时不可以改变,所以说类似C语言的 #define,属于编译期间就确定的内容,而且对数值类型有限制。
直接看代码,对比opcode:
define例子:
<?php
define('FOO', 'foo');
echo FOO;
define opcode:
number of ops: 6
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > SEND_VAL 'FOO'
1 SEND_VAL 'foo'
2 DO_FCALL 2 'define'
3 3 FETCH_CONSTANT ~1 'FOO'
4 ECHO ~1
4 5 > RETURN 1
const例子:
<?php
const FOO = 'foo';
echo FOO;
const opcode:
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > DECLARE_CONST 'FOO', 'foo'
3 1 FETCH_CONSTANT ~0 'FOO'
2 ECHO ~0
4 3 > RETURN 1
### 5.3 动态函数的代价
<?php
function foo() { }
foo();
对应opcode:
number of ops: 3
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 DO_FCALL 0 'foo'
4 2 > RETURN 1
动态调用的代码:
<?php
function foo() { }
$a = 'foo';
$a();
opcode:
number of ops: 5
compiled vars: !0 = $a
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 ASSIGN !0, 'foo'
4 2 INIT_FCALL_BY_NAME !0
3 DO_FCALL_BY_NAME 0
5 4 > RETURN 1
可以 vim Zend/zend_vm_def.h +2630,看看INIT_FCALL_BY_NAME做的事情,代码太长,这里不列出来了。动态特性虽然方便,但一定会牺牲性能,所以使用前要平衡利弊。
### 5.4 类的延迟声明的代价
还是先看代码:
<?php
class Bar { }
class Foo extends Bar { }
对应opcode:
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 NOP
2 NOP
4 3 > RETURN 1
调换声明顺序:
<?php
class Foo extends Bar { }
class Bar { }
对应opcode:
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_CLASS 0 :0 'Bar'
1 DECLARE_INHERITED_CLASS '%00foo%2FUsers%2Fqisen%2Ftmp%2Fvld.php0x103d58020', 'foo'
3 2 NOP
4 3 > RETURN 1
如果在强语言中,后面的写法会产生编译错误,但PHP这种动态语言,会把类的声明推迟到运行时,如果你不注意,就很可能踩到这个雷。
所以在我们了解Zend VM原理后,就更应该注意少用动态特性,可有可无的时候,就一定不要用。
转自:http://www.yinqisen.cn/blog-723.html
先说个PHP5.3+ 的语法糖,通常我们这样写:
<?php
$a = 0;
$b = $a ? $a : 1;
语法糖可以这样写:
<?php
$a = 0;
$b = $a ?: 1;
执行结果$b = 1,后面写法更简洁,但通常不太建议用太多语法糖,特别是容易理解混淆的,比如PHP 7 新增加??如下:
<?php
$b = $a ?? 1;
相当于:
<?php
$b = isset($a) ? $a : 1;
?: 和 ?? 你是不是容易搞混,如果这样,我建议宁可不用,代码可读性强,易维护更重要。
语法糖不是本文的重点,我们的目的是从语法糖入手聊聊Zend VM的解析原理。
## 2.
分析的PHP源码分支 => remotes/origin/PHP-5.6.14,关于如何通过vld查看opcode,请看我之前写的这篇文章:
<http://www.yinqisen.cn/blog-680.html>
<?php
$a = 0;
$b = $a ?: 1;
对应的opcdoe如下:
number of ops: 5
compiled vars: !0 = $a, !1 = $b
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 0
3 1 JMP_SET_VAR $1 !0
2 QM_ASSIGN_VAR $1 1
3 ASSIGN !1, $1
4 4 > RETURN 1
branch: # 0; line: 2- 4; sop: 0; eop: 4; out1: -2
path #1: 0,
vim Zend/zend_language_parser.y +834
~~~.bash
834 › |› expr '?' ':' { zend_do_jmp_set(&$1, &$2, &$3 TSRMLS_CC); }
835 › › expr { zend_do_jmp_set_else(&$$, &$5, &$2, &$3 TSRMLS_CC); }
~~~
如果你喜欢,可以自己动手,重新定义 ?: 的语法糖。遵循BNF文法规则,使用bison解析,有兴趣可以自行Google相关知识,继续深入了解。
从vld的opcode可以知道,执行了 zend_do_jmp_set_else,代码在 Zend/zend_compile.c 中:
~~~.java
void zend_do_jmp_set_else(znode *result, const znode *false_value, const znode *jmp_token, const znode *colon_token TSRMLS_DC)
{
› zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
› SET_NODE(opline->result, colon_token);
› if (colon_token->op_type == IS_TMP_VAR) {
› › if (false_value->op_type == IS_VAR || false_value->op_type == IS_CV) {
› › › CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].opcode = ZEND_JMP_SET_VAR;
› › › CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].result_type = IS_VAR;
› › › opline->opcode = ZEND_QM_ASSIGN_VAR;
› › › opline->result_type = IS_VAR;
› › } else {
› › › opline->opcode = ZEND_QM_ASSIGN;
› › }
› } else {
› › opline->opcode = ZEND_QM_ASSIGN_VAR;
› }
› opline->extended_value = 0;
› SET_NODE(opline->op1, false_value);
› SET_UNUSED(opline->op2);
› GET_NODE(result, opline->result);
› CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].op2.opline_num = get_next_op_number(CG(active_op_array));
› DEC_BPC(CG(active_op_array));
}
~~~
## 3.
重点两个opcode,ZEND_JMP_SET_VAR 和 ZEND_QM_ASSIGN_VAR,怎么接着读代码呢?下面说下PHP的opcode。
PHP5.6有167个opcode,意味着可以执行167种不同的计算操作,官方文档看这里<http://php.net/manual/en/internals2.opcodes.list.php>
PHP内部使用_zend_op 这个结构体来表示opcode, vim Zend/zend_compile.h +111
111 struct _zend_op {
112 › opcode_handler_t handler;
113 › znode_op op1;
114 › znode_op op2;
115 › znode_op result;
116 › ulong extended_value;
117 › uint lineno;
118 › zend_uchar opcode;
119 › zend_uchar op1_type;
120 › zend_uchar op2_type;
121 › zend_uchar result_type;
122 }
PHP 7.0略有不同,主要区别在针对64位系统 uint换成uint32_t,明确指定字节数。
你把opcode当成一个计算器,只接受两个操作数(op1, op2),执行一个操作(handler, 比如加减乘除),然后它返回一个结果(result)给你,再稍加处理算术溢出的情况(extended_value)。
Zend的VM对每个opcode的工作方式完全相同,都有一个handler(函数指针),指向处理函数的地址。这是一个C函数,包含了执行opcode对应的代码,使用op1,op2做为参数,执行完成后,会返回一个结果(result),有时也会附加一段信息(extended_value)。
用我们例子中的操作数 ZEND_JMP_SET_VAR 说明,vim Zend/zend_vm_def.h +4995
4942 ZEND_VM_HANDLER(158, ZEND_JMP_SET_VAR, CONST|TMP|VAR|CV, ANY)
4943 {
4944 › USE_OPLINE
4945 › zend_free_op free_op1;
4946 › zval *value, *ret;
4947
4948 › SAVE_OPLINE();
4949 › value = GET_OP1_ZVAL_PTR(BP_VAR_R);
4950
4951 › if (i_zend_is_true(value)) {
4952 › › if (OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) {
4953 › › › Z_ADDREF_P(value);
4954 › › › EX_T(opline->result.var).var.ptr = value;
4955 › › › EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
4956 › › } else {
4957 › › › ALLOC_ZVAL(ret);
4958 › › › INIT_PZVAL_COPY(ret, value);
4959 › › › EX_T(opline->result.var).var.ptr = ret;
4960 › › › EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
4961 › › › if (!IS_OP1_TMP_FREE()) {
4962 › › › › zval_copy_ctor(EX_T(opline->result.var).var.ptr);
4963 › › › }
4964 › › }
4965 › › FREE_OP1_IF_VAR();
4966 #if DEBUG_ZEND>=2
4967 › › printf("Conditional jmp to %d\n", opline->op2.opline_num);
4968 #endif
4969 › › ZEND_VM_JMP(opline->op2.jmp_addr);
4970 › }
4971
4972 › FREE_OP1();
4973 › CHECK_EXCEPTION();
4974 › ZEND_VM_NEXT_OPCODE();
4975 }
i_zend_is_true 来判断操作数是否为true,所以ZEND_JMP_SET_VAR是一种条件赋值,相信大家都能看明白,下面讲重点。
注意`zend_vm_def.h`这并不是一个可以直接编译的C的头文件,只能说是一个模板,具体可编译的头为`zend_vm_execute.h`(这个文件可有45000多行哦),它并非手动生成,而是由`zend_vm_gen.php`这个PHP脚本解析`zend_vm_def.h`后生成(有意思吧,先有鸡还是先有蛋,没有PHP 哪来的这个脚本?),猜测这个是后期产物,早期php版本应该不会用这个。
上面ZEND_JMP_SET_VAR的代码,根据不同参数 `CONST|TMP|VAR|CV` 最终会生成不同类型的,但功能一致的handler函数:
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static int ZEND_FASTCALL ZEND_JMP_SET_VAR_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
这么做的目的是为了在编译期确定handler,提升运行期的性能。不这么做,在运行期根据参数类型选择,也可以做到,但性能不好。当然这么做有时也会生成一些垃圾代码(看似无用),不用担心,C的编译器会进一步优化处理。
zend_vm_gen.php 也可以接受一些参数,细节在PHP源码中的README文件 `Zend/README.ZEND_VM` 有详细说明。
## 4.
讲到这里,我们知道opcode怎么和handler对应了。但是在整体上还有一个过程,就是语法解析,解析后所有的opcode是怎么串联起来的呢?
语法解析的细节就不说了,解析过后,会有个包含所有opcode的大数组(说链表可能更准确),从上面代码我们可以看到,每个handler执行完后,都会调用 ZEND_VM_NEXT_OPCODE(),取出下一个opcode,继续执行,直到最后退出,循环的代码 vim Zend/zend_vm_execute.h +337:
~~~.java
ZEND_API void execute_ex(zend_execute_data *execute_data TSRMLS_DC)
{
› DCL_OPLINE
› zend_bool original_in_execution;
› original_in_execution = EG(in_execution);
› EG(in_execution) = 1;
› if (0) {
zend_vm_enter:
› › execute_data = i_create_execute_data_from_op_array(EG(active_op_array), 1 TSRMLS_CC);
› }
› LOAD_REGS();
› LOAD_OPLINE();
› while (1) {
› int ret;
#ifdef ZEND_WIN32
› › if (EG(timed_out)) {
› › › zend_timeout(0);
› › }
#endif
› › if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
› › › switch (ret) {
› › › › case 1:
› › › › › EG(in_execution) = original_in_execution;
› › › › › return;
› › › › case 2:
› › › › › goto zend_vm_enter;
› › › › › break;
› › › › case 3:
› › › › › execute_data = EG(current_execute_data);
› › › › › break;
› › › › default:
› › › › › break;
› › › }
› › }
› }
› zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen");
}
~~~
宏定义, vim Zend/zend_execute.c +1772
1772 #define ZEND_VM_NEXT_OPCODE() \
1773 › CHECK_SYMBOL_TABLES() \
1774 › ZEND_VM_INC_OPCODE(); \
1775 › ZEND_VM_CONTINUE()
329 #define ZEND_VM_CONTINUE() return 0
330 #define ZEND_VM_RETURN() return 1
331 #define ZEND_VM_ENTER() return 2
332 #define ZEND_VM_LEAVE() return 3
while是一个死循环,执行一个handler函数,除个别情况,多数handler函数末尾都调用ZEND_VM_NEXT_OPCODE() -> ZEND_VM_CONTINUE(),return 0,继续循环。
> 注:比如 yield 协程是个例外,它会返回1,直接return出循环。以后有机会我们再单独对yield做分析。
希望你看完上面内容,对PHP Zend 引擎的解析过程有个详细的了解,下面我们基于原理的分析,再简单聊聊PHP的优化。
## 5. PHP优化注意事项
### 5.1 echo 输出
<?php
$foo = 'foo';
$bar = 'bar';
echo $foo . $bar;
vld 查看opcode:
number of ops: 5
compiled vars: !0 = $foo, !1 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'foo'
3 1 ASSIGN !1, 'bar'
4 2 CONCAT ~2 !0, !1
3 ECHO ~2
5 4 > RETURN 1
branch: # 0; line: 2- 5; sop: 0; eop: 4; out1: -2
path #1: 0,
ZEND_CONCAT 连接 $a和$b的值,保存到临时变量~2中,然后echo 出来。这个过程中涉及要分配一块内存,用于临时变量,用完后还要释放,还需要调用拼接函数,执行拼接过程。
如果换成这样写:
<?php
$foo = 'foo';
$bar = 'bar';
echo $foo, $bar;
对应的opcode:
number of ops: 5
compiled vars: !0 = $foo, !1 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'foo'
3 1 ASSIGN !1, 'bar'
4 2 ECHO !0
3 ECHO !1
5 4 > RETURN 1
branch: # 0; line: 2- 5; sop: 0; eop: 4; out1: -2
path #1: 0,
不需要分配内存,也不需要执行拼接函数,是不是效率更好呢!想了解拼接过程,可以根据本文讲的内容,自行查找 ZEND_CONCAT 这个opcode对应的handler,做了好多事情哦。
### 5.2 define()和const
const关键字是从5.3开始引入的,和define有很大差别,和C语言的`#define`倒是含义差不多。
* define() 是函数调用,有函数调用开销。
* const 是关键字,直接生成opcode,属于编译期能确定的,不需要动态在执行期分配。
const 的值是死的,运行时不可以改变,所以说类似C语言的 #define,属于编译期间就确定的内容,而且对数值类型有限制。
直接看代码,对比opcode:
define例子:
<?php
define('FOO', 'foo');
echo FOO;
define opcode:
number of ops: 6
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > SEND_VAL 'FOO'
1 SEND_VAL 'foo'
2 DO_FCALL 2 'define'
3 3 FETCH_CONSTANT ~1 'FOO'
4 ECHO ~1
4 5 > RETURN 1
const例子:
<?php
const FOO = 'foo';
echo FOO;
const opcode:
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > DECLARE_CONST 'FOO', 'foo'
3 1 FETCH_CONSTANT ~0 'FOO'
2 ECHO ~0
4 3 > RETURN 1
### 5.3 动态函数的代价
<?php
function foo() { }
foo();
对应opcode:
number of ops: 3
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 DO_FCALL 0 'foo'
4 2 > RETURN 1
动态调用的代码:
<?php
function foo() { }
$a = 'foo';
$a();
opcode:
number of ops: 5
compiled vars: !0 = $a
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 ASSIGN !0, 'foo'
4 2 INIT_FCALL_BY_NAME !0
3 DO_FCALL_BY_NAME 0
5 4 > RETURN 1
可以 vim Zend/zend_vm_def.h +2630,看看INIT_FCALL_BY_NAME做的事情,代码太长,这里不列出来了。动态特性虽然方便,但一定会牺牲性能,所以使用前要平衡利弊。
### 5.4 类的延迟声明的代价
还是先看代码:
<?php
class Bar { }
class Foo extends Bar { }
对应opcode:
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 NOP
2 NOP
4 3 > RETURN 1
调换声明顺序:
<?php
class Foo extends Bar { }
class Bar { }
对应opcode:
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > FETCH_CLASS 0 :0 'Bar'
1 DECLARE_INHERITED_CLASS '%00foo%2FUsers%2Fqisen%2Ftmp%2Fvld.php0x103d58020', 'foo'
3 2 NOP
4 3 > RETURN 1
如果在强语言中,后面的写法会产生编译错误,但PHP这种动态语言,会把类的声明推迟到运行时,如果你不注意,就很可能踩到这个雷。
所以在我们了解Zend VM原理后,就更应该注意少用动态特性,可有可无的时候,就一定不要用。
转自:http://www.yinqisen.cn/blog-723.html
相关推荐
1. PHP生命周期:从源代码解析到执行的过程,包括词法分析、语法分析、编译和执行阶段。 2. Zend引擎详解:深入理解ZEND VM的指令集和执行模型。 3. ZVAL数据结构:探究PHP变量的内部表示及其动态类型转换机制。 4. ...
4. **Zend虚拟机(Zend VM)**:作为PHP的执行引擎,Zend VM是如何工作的?它如何将PHP源代码转换成字节码并执行?书中会详细介绍Zend VM的指令集、执行流程和优化策略。 5. **扩展开发**:如何编写PHP扩展?这包括...
ZEND_ECHO操作码在`zend_vm_def.h`头文件中定义,并在虚拟机中通过一个处理函数处理。这个处理函数检查要输出的变量类型,如果变量是对象,它会先将其转换为字符串,然后调用`zend_print_variable()`进行输出。 `...
2. **语法分析**:接着,词法分析后的Token被传递给语法分析器(由yacc生成,源文件在Zend/zend_language_parser.y)。语法分析器将Token转化为opcode(操作码),这是PHP的中间语言。Opcode以op array的形式存在,...
书中还可能探讨了ZEND VM(Zend Engine的虚拟机)的工作原理,以及如何通过扩展自定义行为。 再者,执行阶段是PHP生命周期中的关键步骤。书中会解释如何执行字节码,包括执行循环、控制流、内存管理、对象模型等。...
1. `Zend`目录:这是PHP的核心引擎,负责解释和执行PHP脚本。它包含了VM(虚拟机)实现、编译器、执行器等相关组件。 2. `ext`目录:包含所有内置扩展,如`mysqli`(MySQL数据库接口)、`pdo`(PHP数据对象)、`...
5. **执行**:Zend VM(虚拟机)执行这些字节码,通过操作符栈和各种内建函数来处理计算和控制流。 C++源码还可能涉及以下关键组件: - **内存管理**:包括垃圾回收算法,确保有效管理PHP运行时的内存分配和释放。...
6. PHP虚拟机:PHP有自己的ZEND虚拟机,CompxlerPHP可能需要理解和操作ZEND VM的指令集。 7. 开源社区:作为PHP项目,可能涉及版本控制、文档编写、单元测试以及如何与开源社区互动。 8. PHP版本兼容性:考虑到PHP...
PHP7通过引入全新的ZEND VM(Zend Engine 3.0)和改进的类型系统,大大提升了执行效率。与PHP5相比,PHP7的运行速度提高了两倍,内存消耗也有所减少。这使得开发者在编写高效代码时有了更多的选择。 2. **空合并...
PHP5.4在性能方面进行了优化,如对ZEND VM(Zend Engine Virtual Machine)的改进,提高了脚本执行速度。此外,新的OPcache(Opcode Cache)提供了预编译的PHP脚本缓存,进一步提升了性能。 9. **面向对象的改进**...
通过分析Zend Engine的源码,我们可以学习到如何实现动态语言的解释器,以及内存管理策略。 3. **扩展机制** PHP的扩展机制允许开发者添加自定义功能,如MySQLi、PDO等数据库驱动。在“ext”目录下,每个扩展都有...
2. **编译成字节码(Opcode)**:PHP可以被编译成中间字节码,如PHP的ZEND VM。这种方式使得源代码不能直接被人类阅读,但仍然可以通过反编译工具进行分析。 3. **第三方加密库**:市面上存在一些商业的PHP加密库,...
- **语法分析(Parsing)**:接着,这些标记被解析器(parser)转化为抽象语法树(AST),这是一个逻辑结构,代表了程序的结构和语义。 - **编译(Compilation)**:PHP的编译过程不是将源代码转化为机器码,而是...
书中会讲解ZEND VM(Zend虚拟机)的工作方式,它是PHP执行引擎的基础。通过学习ZEND VM,读者可以理解变量的生命周期、内存栈的管理以及垃圾回收机制,这些对优化代码性能至关重要。 再者,PHP的函数调用和对象模型...
注释涵盖了从语法解析器(Tokenizer和Parser)、符号表管理(ZEND Engine的符号表)到内存管理、垃圾回收算法等多个关键模块。 1. **Tokenizer**:PHP的词法分析器将输入的源代码转换成一个个Token,这些Token是...