从 PHP 语法糖剖析 Zend VM 引擎 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
heitao1

从 PHP 语法糖剖析 Zend VM 引擎

  •  1
     
  •   heitao1 2016 年 11 月 4 日 3054 次点击
    这是一个创建于 3458 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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

    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 中:

    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 :

    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_CONAT 这个 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

    8 条回复    2016-11-07 22:06:33 +08:00
    wujunze
        1
    wujunze  
       2016 年 11 月 4 日
    感谢分享
    Light3
        2
    Light3  
       2016 年 11 月 4 日
    吊吊吊
    lslqtz
        3
    lslqtz  
       2016 年 11 月 4 日
    感谢分享
    young
        4
    young  
       2016 年 11 月 4 日
    666
    ylsc633
        5
    ylsc633  
       2016 年 11 月 4 日
    正好是我要看的! 先马
    kideny
        6
    kideny  
       2016 年 11 月 4 日
    战略性 mark
    t0byxdd
        7
    t0byxdd  
       2016 年 11 月 4 日
    mark
    mingyun
        8
    mingyun  
       2016 年 11 月 7 日
    厉害了我的哥
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2839 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 82ms UTC 14:53 PVG 22:53 LAX 07:53 JFK 10:53
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86