從PHP文法糖剖析Zend VM引擎

來源:互聯網
上載者:User

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 '?' ':' { 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_OPLINE4945 ›   zend_free_op free_op1;4946 ›   zval *value, *ret;49474948 ›   SAVE_OPLINE();4949 ›   value = GET_OP1_ZVAL_PTR(BP_VAR_R);49504951 ›   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>=24967 ›   ›   printf("Conditional jmp to %d\n", opline->op2.opline_num);4968 #endif4969 ›   ›   ZEND_VM_JMP(opline->op2.jmp_addr);4970 ›   }49714972 ›   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 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 = 'foo';    $bar = 'bar';    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, 'foo'   3     1        ASSIGN                                                   !1, 'bar'   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 = 'foo';    $bar = 'bar';    echo $foo, $bar;

對應的opcode:

number of ops:  5compiled vars:  !0 = $foo, !1 = $barline     #* 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                                                   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('FOO', 'foo');    echo FOO;

define opcode:

number of ops:  6compiled vars:  noneline     #* 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:  4compiled vars:  noneline     #* 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:  3compiled vars:  noneline     #* 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:  5compiled vars:  !0 = $aline     #* 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:  4compiled vars:  noneline     #* 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:  4compiled vars:  noneline     #* 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原理後,就更應該注意少用動態特性,可有可無的時候,就一定不要用。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.