从PHP语法糖剖析Zend VM引擎

巴扎黑
发布: 2016-11-21 09:46:14
原创
1296人浏览过

## 1. 

先说个php5.3+ 的语法糖,通常我们这样写: 

            $a = 0; 
        $b = $a ? $a : 1; 

语法糖可以这样写: 

            $a = 0; 
        $b = $a ?: 1; 

执行结果$b = 1,后面写法更简洁,但通常不太建议用太多语法糖,特别是容易理解混淆的,比如php 7 新增加??如下: 

            $b = $a ?? 1; 

相当于: 

            $b = isset($a) ? $a : 1; 

?: 和 ?? 你是不是容易搞混,如果这样,我建议宁可不用,代码可读性强,易维护更重要。 

语法糖不是本文的重点,我们的目的是从语法糖入手聊聊zend vm的解析原理。 

## 2. 

分析的php源码分支 => remotes/origin/php-5.6.14,关于如何通过vld查看opcode,请看我之前写的这篇文章:  
 

            $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种不同的计算操作,官方文档看这里 

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 输出 

            $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 出来。这个过程中涉及要分配一块内存,用于临时变量,用完后还要释放,还需要调用拼接函数,执行拼接过程。 

如果换成这样写: 

            $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例子: 

            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例子: 

            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 动态函数的代价 

            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 

动态调用的代码: 

            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 类的延迟声明的代价 

还是先看代码: 

            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 

调换声明顺序: 

            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原理后,就更应该注意少用动态特性,可有可无的时候,就一定不要用。 

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号