免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 3796 | 回复: 1
打印 上一主题 下一主题

从PHP语法糖剖析Zend VM引擎 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2016-11-04 11:11 |只看该作者 |倒序浏览
本帖最后由 wyk86485481 于 2016-11-04 11:44 编辑

1.

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

  1. <?php
  2.     $a = 0;
  3.     $b = $a ? $a : 1;
复制代码

语法糖可以这样写:

  1. <?php
  2.     $a = 0;
  3.     $b = $a ?: 1;
复制代码

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

  1. <?php
  2.     $b = $a ?? 1;
复制代码

相当于:

  1. <?php
  2.     $b = isset($a) ? $a : 1;
复制代码

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

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

2.

分析的PHP源码分支 => remotes/origin/PHP-5.6.14,关于如何通过vld查看opcode,请看我之前写的这篇文章:
http://www.yinqisen.cn/blog-680.html

  1. <?php
  2.     $a = 0;
  3.     $b = $a ?: 1;
复制代码

对应的opcdoe如下:

  1. number of ops:  5
  2. compiled vars:  !0 = $a, !1 = $b
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   ASSIGN                                                   !0, 0
  6.    3     1        JMP_SET_VAR                                      $1      !0
  7.          2        QM_ASSIGN_VAR                                    $1      1
  8.          3        ASSIGN                                                   !1, $1
  9.    4     4      > RETURN                                                   1

  10. branch: #  0; line:     2-    4; sop:     0; eop:     4; out1:  -2
  11. path #1: 0,
复制代码

vim Zend/zend_language_parser.y +834

  1. 834 ›   |›  expr '?' ':' { zend_do_jmp_set(&$1, &$2, &$3 TSRMLS_CC); }
  2. 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 中:

  1. void zend_do_jmp_set_else(znode *result, const znode *false_value, const znode *jmp_token, const znode *colon_token TSRMLS_DC)
  2. {
  3. ›   zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

  4. ›   SET_NODE(opline->result, colon_token);
  5. ›   if (colon_token->op_type == IS_TMP_VAR) {
  6. ›   ›   if (false_value->op_type == IS_VAR || false_value->op_type == IS_CV) {
  7. ›   ›   ›   CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].opcode = ZEND_JMP_SET_VAR;
  8. ›   ›   ›   CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].result_type = IS_VAR;
  9. ›   ›   ›   opline->opcode = ZEND_QM_ASSIGN_VAR;
  10. ›   ›   ›   opline->result_type = IS_VAR;
  11. ›   ›   } else {
  12. ›   ›   ›   opline->opcode = ZEND_QM_ASSIGN;
  13. ›   ›   }
  14. ›   } else {
  15. ›   ›   opline->opcode = ZEND_QM_ASSIGN_VAR;
  16. ›   }
  17. ›   opline->extended_value = 0;
  18. ›   SET_NODE(opline->op1, false_value);
  19. ›   SET_UNUSED(opline->op2);

  20. ›   GET_NODE(result, opline->result);

  21. ›   CG(active_op_array)->opcodes[jmp_token->u.op.opline_num].op2.opline_num = get_next_op_number(CG(active_op_array));

  22. ›   DEC_BPC(CG(active_op_array));
  23. }
复制代码

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

  1. 111 struct _zend_op {
  2. 112 ›   opcode_handler_t handler;
  3. 113 ›   znode_op op1;
  4. 114 ›   znode_op op2;
  5. 115 ›   znode_op result;
  6. 116 ›   ulong extended_value;
  7. 117 ›   uint lineno;
  8. 118 ›   zend_uchar opcode;
  9. 119 ›   zend_uchar op1_type;
  10. 120 ›   zend_uchar op2_type;
  11. 121 ›   zend_uchar result_type;
  12. 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

  1. 4942 ZEND_VM_HANDLER(158, ZEND_JMP_SET_VAR, CONST|TMP|VAR|CV, ANY)
  2. 4943 {
  3. 4944 ›   USE_OPLINE
  4. 4945 ›   zend_free_op free_op1;
  5. 4946 ›   zval *value, *ret;
  6. 4947
  7. 4948 ›   SAVE_OPLINE();
  8. 4949 ›   value = GET_OP1_ZVAL_PTR(BP_VAR_R);
  9. 4950
  10. 4951 ›   if (i_zend_is_true(value)) {
  11. 4952 ›   ›   if (OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) {
  12. 4953 ›   ›   ›   Z_ADDREF_P(value);
  13. 4954 ›   ›   ›   EX_T(opline->result.var).var.ptr = value;
  14. 4955 ›   ›   ›   EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
  15. 4956 ›   ›   } else {
  16. 4957 ›   ›   ›   ALLOC_ZVAL(ret);
  17. 4958 ›   ›   ›   INIT_PZVAL_COPY(ret, value);
  18. 4959 ›   ›   ›   EX_T(opline->result.var).var.ptr = ret;
  19. 4960 ›   ›   ›   EX_T(opline->result.var).var.ptr_ptr = &EX_T(opline->result.var).var.ptr;
  20. 4961 ›   ›   ›   if (!IS_OP1_TMP_FREE()) {
  21. 4962 ›   ›   ›   ›   zval_copy_ctor(EX_T(opline->result.var).var.ptr);
  22. 4963 ›   ›   ›   }
  23. 4964 ›   ›   }
  24. 4965 ›   ›   FREE_OP1_IF_VAR();
  25. 4966 #if DEBUG_ZEND>=2
  26. 4967 ›   ›   printf("Conditional jmp to %d\n", opline->op2.opline_num);
  27. 4968 #endif
  28. 4969 ›   ›   ZEND_VM_JMP(opline->op2.jmp_addr);
  29. 4970 ›   }
  30. 4971
  31. 4972 ›   FREE_OP1();
  32. 4973 ›   CHECK_EXCEPTION();
  33. 4974 ›   ZEND_VM_NEXT_OPCODE();
  34. 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函数:

  1. static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
  2. static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
  3. static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
  4. 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:

  1. ZEND_API void execute_ex(zend_execute_data *execute_data TSRMLS_DC)
  2. {
  3. ›   DCL_OPLINE
  4. ›   zend_bool original_in_execution;



  5. ›   original_in_execution = EG(in_execution);
  6. ›   EG(in_execution) = 1;

  7. ›   if (0) {
  8. zend_vm_enter:
  9. ›   ›   execute_data = i_create_execute_data_from_op_array(EG(active_op_array), 1 TSRMLS_CC);
  10. ›   }

  11. ›   LOAD_REGS();
  12. ›   LOAD_OPLINE();

  13. ›   while (1) {
  14.     ›   int ret;
  15. #ifdef ZEND_WIN32
  16. ›   ›   if (EG(timed_out)) {
  17. ›   ›   ›   zend_timeout(0);
  18. ›   ›   }
  19. #endif

  20. ›   ›   if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
  21. ›   ›   ›   switch (ret) {
  22. ›   ›   ›   ›   case 1:
  23. ›   ›   ›   ›   ›   EG(in_execution) = original_in_execution;
  24. ›   ›   ›   ›   ›   return;
  25. ›   ›   ›   ›   case 2:
  26. ›   ›   ›   ›   ›   goto zend_vm_enter;
  27. ›   ›   ›   ›   ›   break;
  28. ›   ›   ›   ›   case 3:
  29. ›   ›   ›   ›   ›   execute_data = EG(current_execute_data);
  30. ›   ›   ›   ›   ›   break;
  31. ›   ›   ›   ›   default:
  32. ›   ›   ›   ›   ›   break;
  33. ›   ›   ›   }
  34. ›   ›   }

  35. ›   }
  36. ›   zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen");
  37. }
复制代码

宏定义, vim Zend/zend_execute.c +1772

  1. 1772 #define ZEND_VM_NEXT_OPCODE() \
  2. 1773 ›   CHECK_SYMBOL_TABLES() \
  3. 1774 ›   ZEND_VM_INC_OPCODE(); \
  4. 1775 ›   ZEND_VM_CONTINUE()

  5. 329 #define ZEND_VM_CONTINUE()         return 0
  6. 330 #define ZEND_VM_RETURN()           return 1
  7. 331 #define ZEND_VM_ENTER()            return 2
  8. 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 输出

  1. <?php
  2.     $foo = 'foo';
  3.     $bar = 'bar';
  4.     echo $foo . $bar;
复制代码

vld 查看opcode:

  1. number of ops:  5
  2. compiled vars:  !0 = $foo, !1 = $bar
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   ASSIGN                                                   !0, 'foo'
  6.    3     1        ASSIGN                                                   !1, 'bar'
  7.    4     2        CONCAT                                           ~2      !0, !1
  8.          3        ECHO                                                     ~2
  9.    5     4      > RETURN                                                   1

  10. branch: #  0; line:     2-    5; sop:     0; eop:     4; out1:  -2
  11. path #1: 0,
复制代码

ZEND_CONCAT 连接 $a和$b的值,保存到临时变量~2中,然后echo 出来。这个过程中涉及要分配一块内存,用于临时变量,用完后还要释放,还需要调用拼接函数,执行拼接过程。

如果换成这样写:

  1. <?php
  2.     $foo = 'foo';
  3.     $bar = 'bar';
  4.     echo $foo, $bar;
复制代码

对应的opcode:

  1. number of ops:  5
  2. compiled vars:  !0 = $foo, !1 = $bar
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   ASSIGN                                                   !0, 'foo'
  6.    3     1        ASSIGN                                                   !1, 'bar'
  7.    4     2        ECHO                                                     !0
  8.          3        ECHO                                                     !1
  9.    5     4      > RETURN                                                   1

  10. branch: #  0; line:     2-    5; sop:     0; eop:     4; out1:  -2
  11. 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例子:

  1. <?php
  2.     define('FOO', 'foo');
  3.     echo FOO;
复制代码

define opcode:

  1. number of ops:  6
  2. compiled vars:  none
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   SEND_VAL                                                 'FOO'
  6.          1        SEND_VAL                                                 'foo'
  7.          2        DO_FCALL                                      2          'define'
  8.    3     3        FETCH_CONSTANT                                   ~1      'FOO'
  9.          4        ECHO                                                     ~1
  10.    4     5      > RETURN                                                   1
复制代码

const例子:

  1. <?php
  2.     const FOO = 'foo';
  3.     echo FOO;
复制代码

const opcode:

  1. number of ops:  4
  2. compiled vars:  none
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   DECLARE_CONST                                            'FOO', 'foo'
  6.    3     1        FETCH_CONSTANT                                   ~0      'FOO'
  7.          2        ECHO                                                     ~0
  8.    4     3      > RETURN                                                   1
复制代码

5.3 动态函数的代价

  1. <?php
  2.     function foo() { }
  3.     foo();
复制代码

对应opcode:

  1. number of ops:  3
  2. compiled vars:  none
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   NOP
  6.    3     1        DO_FCALL                                      0          'foo'
  7.    4     2      > RETURN                                                   1
复制代码

动态调用的代码:

  1. <?php
  2.     function foo() opcode: }
  3.     $a = 'foo';
  4.     $a();
复制代码

opcode:

  1. number of ops:  5
  2. compiled vars:  !0 = $a
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   NOP
  6.    3     1        ASSIGN                                                   !0, 'foo'
  7.    4     2        INIT_FCALL_BY_NAME                                       !0
  8.          3        DO_FCALL_BY_NAME                              0
  9.    5     4      > RETURN                                                   1
复制代码

可以 vim Zend/zend_vm_def.h +2630,看看INIT_FCALL_BY_NAME做的事情,代码太长,这里不列出来了。动态特性虽然方便,但一定会牺牲性能,所以使用前要平衡利弊。

5.4 类的延迟声明的代价
还是先看代码:

  1. <?php
  2.     class Bar { }
  3.     class Foo extends Bar { }
复制代码

对应opcode:

  1. number of ops:  4
  2. compiled vars:  none
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   NOP
  6.    3     1        NOP
  7.          2        NOP
  8.    4     3      > RETURN                                                   1
复制代码

调换声明顺序:

  1. <?php
  2.     class Foo extends Bar { }
  3.     class Bar { }
复制代码

对应opcode:

  1. number of ops:  4
  2. compiled vars:  none
  3. line     #* E I O op                           fetch          ext  return  operands
  4. -------------------------------------------------------------------------------------
  5.    2     0  E >   FETCH_CLASS                                   0  :0      'Bar'
  6.          1        DECLARE_INHERITED_CLASS                                  '%00foo%2FUsers%2Fqisen%2Ftmp%2Fvld.php0x103d58020', 'foo'
  7.    3     2        NOP
  8.    4     3      > RETURN                                                   1
复制代码

如果在强语言中,后面的写法会产生编译错误,但PHP这种动态语言,会把类的声明推迟到运行时,如果你不注意,就很可能踩到这个雷。

所以在我们了解Zend VM原理后,就更应该注意少用动态特性,可有可无的时候,就一定不要用。


论坛徽章:
0
2 [报告]
发表于 2017-02-06 15:23 |只看该作者
围观
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP