搜索
首页后端开发php教程从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:  5compiled vars:  !0 = $a, !1 = $bline     #* 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                                                   1branch: #  0; line:     2-    4; sop:     0; eop:     4; out1:  -2path #1: 0,

vim Zend/zend_language_parser.y +834

834 ›   |›  expr &#39;?&#39; &#39;:&#39; { 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)

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&#39;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 0330 #define ZEND_VM_RETURN()           return 1331 #define ZEND_VM_ENTER()            return 2332 #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 = &#39;foo&#39;;
    $bar = &#39;bar&#39;;    echo $foo . $bar;

vld 查看opcode:

number of ops:  5compiled vars:  !0 = $foo, !1 = $barline     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------   2     0  E >   ASSIGN                                                   !0, &#39;foo&#39;
   3     1        ASSIGN                                                   !1, &#39;bar&#39;
   4     2        CONCAT                                           ~2      !0, !1
         3        ECHO                                                     ~2
   5     4      > RETURN                                                   1branch: #  0; line:     2-    5; sop:     0; eop:     4; out1:  -2path #1: 0,

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

如果换成这样写:

<?php
    $foo = &#39;foo&#39;;
    $bar = &#39;bar&#39;;    echo $foo, $bar;

对应的opcode:

number of ops:  5compiled vars:  !0 = $foo, !1 = $bar
line     #* E I O op                           fetch          ext  return  operands-------------------------------------------------------------------------------------   2     0  E >   ASSIGN                                                   !0, &#39;foo&#39;
   3     1        ASSIGN                                                   !1, &#39;bar&#39;
   4     2        ECHO                                                     !0
         3        ECHO                                                     !1
   5     4      > RETURN                                                   1branch: #  0; line:     2-    5; sop:     0; eop:     4; out1:  -2path #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(&#39;FOO&#39;, &#39;foo&#39;);    echo FOO;

define opcode:

number of ops:  6compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------   
2     0  E >   SEND_VAL                                                 &#39;FOO&#39;
         1        SEND_VAL                                                 &#39;foo&#39;
         2        DO_FCALL                                      2          &#39;define&#39;
   3     3        FETCH_CONSTANT                                   ~1      &#39;FOO&#39;
         4        ECHO                                                     ~1
   4     5      > RETURN                                                   1

const例子:

<?php
    const FOO = &#39;foo&#39;;    echo FOO;

const opcode:

number of ops:  4compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------  
 2     0  E >   DECLARE_CONST                                            &#39;FOO&#39;, &#39;foo&#39;
   3     1        FETCH_CONSTANT                                   ~0      &#39;FOO&#39;
         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          &#39;foo&#39;
   4     2      > RETURN                                                   1

动态调用的代码:

<?php
    function foo() { }
    $a = &#39;foo&#39;;
    $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, &#39;foo&#39;
   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

调换声明顺序:

<?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      &#39;Bar&#39;
         1        DECLARE_INHERITED_CLASS                                  &#39;%00foo%2FUsers%2Fqisen%2Ftmp%2Fvld.php0x103d58020&#39;, &#39;foo&#39;
   3     2        NOP
   4     3      > RETURN                                                   1

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

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


声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
使用数据库存储会话的优点是什么?使用数据库存储会话的优点是什么?Apr 24, 2025 am 12:16 AM

使用数据库存储会话的主要优势包括持久性、可扩展性和安全性。1.持久性:即使服务器重启,会话数据也能保持不变。2.可扩展性:适用于分布式系统,确保会话数据在多服务器间同步。3.安全性:数据库提供加密存储,保护敏感信息。

您如何在PHP中实现自定义会话处理?您如何在PHP中实现自定义会话处理?Apr 24, 2025 am 12:16 AM

在PHP中实现自定义会话处理可以通过实现SessionHandlerInterface接口来完成。具体步骤包括:1)创建实现SessionHandlerInterface的类,如CustomSessionHandler;2)重写接口中的方法(如open,close,read,write,destroy,gc)来定义会话数据的生命周期和存储方式;3)在PHP脚本中注册自定义会话处理器并启动会话。这样可以将数据存储在MySQL、Redis等介质中,提升性能、安全性和可扩展性。

什么是会话ID?什么是会话ID?Apr 24, 2025 am 12:13 AM

SessionID是网络应用程序中用来跟踪用户会话状态的机制。1.它是一个随机生成的字符串,用于在用户与服务器之间的多次交互中保持用户的身份信息。2.服务器生成并通过cookie或URL参数发送给客户端,帮助在用户的多次请求中识别和关联这些请求。3.生成通常使用随机算法保证唯一性和不可预测性。4.在实际开发中,可以使用内存数据库如Redis来存储session数据,提升性能和安全性。

您如何在无状态环境(例如API)中处理会议?您如何在无状态环境(例如API)中处理会议?Apr 24, 2025 am 12:12 AM

在无状态环境如API中管理会话可以通过使用JWT或cookies来实现。1.JWT适合无状态和可扩展性,但大数据时体积大。2.Cookies更传统且易实现,但需谨慎配置以确保安全性。

您如何防止与会议有关的跨站点脚本(XSS)攻击?您如何防止与会议有关的跨站点脚本(XSS)攻击?Apr 23, 2025 am 12:16 AM

要保护应用免受与会话相关的XSS攻击,需采取以下措施:1.设置HttpOnly和Secure标志保护会话cookie。2.对所有用户输入进行输出编码。3.实施内容安全策略(CSP)限制脚本来源。通过这些策略,可以有效防护会话相关的XSS攻击,确保用户数据安全。

您如何优化PHP会话性能?您如何优化PHP会话性能?Apr 23, 2025 am 12:13 AM

优化PHP会话性能的方法包括:1.延迟会话启动,2.使用数据库存储会话,3.压缩会话数据,4.管理会话生命周期,5.实现会话共享。这些策略能显着提升应用在高并发环境下的效率。

什么是session.gc_maxlifetime配置设置?什么是session.gc_maxlifetime配置设置?Apr 23, 2025 am 12:10 AM

thesession.gc_maxlifetimesettinginphpdeterminesthelifespanofsessiondata,setInSeconds.1)它'sconfiguredinphp.iniorviaini_set().2)abalanceIsiseededeedeedeedeedeedeedto to to avoidperformance andununununununexpectedLogOgouts.3)

您如何在PHP中配置会话名?您如何在PHP中配置会话名?Apr 23, 2025 am 12:08 AM

在PHP中,可以使用session_name()函数配置会话名称。具体步骤如下:1.使用session_name()函数设置会话名称,例如session_name("my_session")。2.在设置会话名称后,调用session_start()启动会话。配置会话名称可以避免多应用间的会话数据冲突,并增强安全性,但需注意会话名称的唯一性、安全性、长度和设置时机。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器

Atom编辑器mac版下载

Atom编辑器mac版下载

最流行的的开源编辑器

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!