技术渣 2016-11-04
##1.
先说个PHP5.3+的语法糖,通常我们这样写:
<?php
$a=0;
$b=$a?$a:1;
语法糖可以这样写:
<?php
$a=0;
$b=$a?:1;
执行结果$b=1,后面写法更简洁,但通常不太建议用太多语法糖,特别是容易理解混淆的,比如PHP7新增加??如下:
<?php
$b=$a??1;
相当于:
<?php
$b=isset($a)?$a:1;
?:和??你是不是容易搞混,如果这样,我建议宁可不用,代码可读性强,易维护更重要。
语法糖不是本文的重点,我们的目的是从语法糖入手聊聊ZendVM的解析原理。
##2.
分析的PHP源码分支=>remotes/origin/PHP-5.6.14,关于如何通过vld查看opcode,请看我之前写的这篇文章:
<http://www.yinqisen.cn/blog-680.html>
<?php
$a=0;
$b=$a?:1;
对应的opcdoe如下:
numberofops:5
compiledvars:!0=$a,!1=$b
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>ASSIGN!0,0
31JMP_SET_VAR$1!0
2QM_ASSIGN_VAR$11
3ASSIGN!1,$1
44>RETURN1
branch:#0;line:2-4;sop:0;eop:4;out1:-2
path#1:0,
vimZend/zend_language_parser.y+834
~~~.bash
834›|›expr'?'':'{zend_do_jmp_set(&$1,&$2,&$3TSRMLS_CC);}
835››expr{zend_do_jmp_set_else(&$$,&$5,&$2,&$3TSRMLS_CC);}
~~~
如果你喜欢,可以自己动手,重新定义?:的语法糖。遵循BNF文法规则,使用bison解析,有兴趣可以自行Google相关知识,继续深入了解。
从vld的opcode可以知道,执行了zend_do_jmp_set_else,代码在Zend/zend_compile.c中:
~~~.java
voidzend_do_jmp_set_else(znode*result,constznode*false_value,constznode*jmp_token,constznode*colon_tokenTSRMLS_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,vimZend/zend_compile.h+111
111struct_zend_op{
112›opcode_handler_thandler;
113›znode_opop1;
114›znode_opop2;
115›znode_opresult;
116›ulongextended_value;
117›uintlineno;
118›zend_ucharopcode;
119›zend_ucharop1_type;
120›zend_ucharop2_type;
121›zend_ucharresult_type;
122}
PHP7.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说明,vimZend/zend_vm_def.h+4995
4942ZEND_VM_HANDLER(158,ZEND_JMP_SET_VAR,CONST|TMP|VAR|CV,ANY)
4943{
4944›USE_OPLINE
4945›zend_free_opfree_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#ifDEBUG_ZEND>=2
4967››printf("Conditionaljmpto%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函数:
staticintZEND_FASTCALLZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
staticintZEND_FASTCALLZEND_JMP_SET_VAR_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
staticintZEND_FASTCALLZEND_JMP_SET_VAR_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
staticintZEND_FASTCALLZEND_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,继续执行,直到最后退出,循环的代码vimZend/zend_vm_execute.h+337:
~~~.java
ZEND_APIvoidexecute_ex(zend_execute_data*execute_dataTSRMLS_DC)
{
›DCL_OPLINE
›zend_booloriginal_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),1TSRMLS_CC);
›}
›LOAD_REGS();
›LOAD_OPLINE();
›while(1){
›intret;
#ifdefZEND_WIN32
››if(EG(timed_out)){
›››zend_timeout(0);
››}
#endif
››if((ret=OPLINE->handler(execute_dataTSRMLS_CC))>0){
›››switch(ret){
››››case1:
›››››EG(in_execution)=original_in_execution;
›››››return;
››››case2:
›››››gotozend_vm_enter;
›››››break;
››››case3:
›››››execute_data=EG(current_execute_data);
›››››break;
››››default:
›››››break;
›››}
››}
›}
›zend_error_noreturn(E_ERROR,"Arrivedatendofmainloopwhichshouldn'thappen");
}
~~~
宏定义,vimZend/zend_execute.c+1772
1772#defineZEND_VM_NEXT_OPCODE()\
1773›CHECK_SYMBOL_TABLES()\
1774›ZEND_VM_INC_OPCODE();\
1775›ZEND_VM_CONTINUE()
329#defineZEND_VM_CONTINUE()return0
330#defineZEND_VM_RETURN()return1
331#defineZEND_VM_ENTER()return2
332#defineZEND_VM_LEAVE()return3
while是一个死循环,执行一个handler函数,除个别情况,多数handler函数末尾都调用ZEND_VM_NEXT_OPCODE()->ZEND_VM_CONTINUE(),return0,继续循环。
>注:比如yield协程是个例外,它会返回1,直接return出循环。以后有机会我们再单独对yield做分析。
希望你看完上面内容,对PHPZend引擎的解析过程有个详细的了解,下面我们基于原理的分析,再简单聊聊PHP的优化。
##5.PHP优化注意事项
###5.1echo输出
<?php
$foo='foo';
$bar='bar';
echo$foo.$bar;
vld查看opcode:
numberofops:5
compiledvars:!0=$foo,!1=$bar
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>ASSIGN!0,'foo'
31ASSIGN!1,'bar'
42CONCAT~2!0,!1
3ECHO~2
54>RETURN1
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:
numberofops:5
compiledvars:!0=$foo,!1=$bar
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>ASSIGN!0,'foo'
31ASSIGN!1,'bar'
42ECHO!0
3ECHO!1
54>RETURN1
branch:#0;line:2-5;sop:0;eop:4;out1:-2
path#1:0,
不需要分配内存,也不需要执行拼接函数,是不是效率更好呢!想了解拼接过程,可以根据本文讲的内容,自行查找ZEND_CONCAT这个opcode对应的handler,做了好多事情哦。
###5.2define()和const
const关键字是从5.3开始引入的,和define有很大差别,和C语言的`#define`倒是含义差不多。
*define()是函数调用,有函数调用开销。
*const是关键字,直接生成opcode,属于编译期能确定的,不需要动态在执行期分配。
const的值是死的,运行时不可以改变,所以说类似C语言的#define,属于编译期间就确定的内容,而且对数值类型有限制。
直接看代码,对比opcode:
define例子:
<?php
define('FOO','foo');
echoFOO;
defineopcode:
numberofops:6
compiledvars:none
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>SEND_VAL'FOO'
1SEND_VAL'foo'
2DO_FCALL2'define'
33FETCH_CONSTANT~1'FOO'
4ECHO~1
45>RETURN1
const例子:
<?php
constFOO='foo';
echoFOO;
constopcode:
numberofops:4
compiledvars:none
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>DECLARE_CONST'FOO','foo'
31FETCH_CONSTANT~0'FOO'
2ECHO~0
43>RETURN1
###5.3动态函数的代价
<?php
functionfoo(){}
foo();
对应opcode:
numberofops:3
compiledvars:none
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>NOP
31DO_FCALL0'foo'
42>RETURN1
动态调用的代码:
<?php
functionfoo(){}
$a='foo';
$a();
opcode:
numberofops:5
compiledvars:!0=$a
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>NOP
31ASSIGN!0,'foo'
42INIT_FCALL_BY_NAME!0
3DO_FCALL_BY_NAME0
54>RETURN1
可以vimZend/zend_vm_def.h+2630,看看INIT_FCALL_BY_NAME做的事情,代码太长,这里不列出来了。动态特性虽然方便,但一定会牺牲性能,所以使用前要平衡利弊。
###5.4类的延迟声明的代价
还是先看代码:
<?php
classBar{}
classFooextendsBar{}
对应opcode:
numberofops:4
compiledvars:none
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>NOP
31NOP
2NOP
43>RETURN1
调换声明顺序:
<?php
classFooextendsBar{}
classBar{}
对应opcode:
numberofops:4
compiledvars:none
line#*EIOopfetchextreturnoperands
-------------------------------------------------------------------------------------
20E>FETCH_CLASS0:0'Bar'
1DECLARE_INHERITED_CLASS'%00foo%2FUsers%2Fqisen%2Ftmp%2Fvld.php0x103d58020','foo'
32NOP
43>RETURN1
如果在强语言中,后面的写法会产生编译错误,但PHP这种动态语言,会把类的声明推迟到运行时,如果你不注意,就很可能踩到这个雷。
所以在我们了解ZendVM原理后,就更应该注意少用动态特性,可有可无的时候,就一定不要用。
转自:http://www.yinqisen.cn/blog-723.html
<?php. if (!empty($_POST)) {. $data1 = $_POST["data1"];$data2 = $_POST["data2"];$fuhao = $_POST["fuh