首頁  >  文章  >  後端開發  >  從PHP語法糖剖析Zend VM引擎

從PHP語法糖剖析Zend VM引擎

巴扎黑
巴扎黑原創
2016-11-21 09:46:141124瀏覽

## 1. 

先說個PHP5.3+ 的語法糖,通常我們這樣寫: 

            $a = ?php 
        $a = ?php 
        $a = ?php 
   
語法糖可以這樣寫: 

            $a = 0; 
        $b = $a ?: 1;         $b = $a ?: 1; 特別是容易理解混淆的,例如PHP 7 新增加??如下: 

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

?: 和?? 你是不是容易搞混,如果這樣,我建議寧可不用,代碼可讀性強,易維護更重要。 

語法糖不是本文的重點,我們的目的是從語法糖入手聊聊Zend VM的解析原理。

## 2. 

分析的PHP原始碼分支=> remotes/origin/PHP-5.6.14,關於如何透過vld查看opcode,請看我之前寫的這篇文章:  
 

            $a = 0; 
      number of ops:  5 
    compiled vars:  ! 0 = $a, !1 = $b 
    line     #* E I O op                 ands 
    ----------------------------- -------------------------------------------------- ------ 
       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, ~~~ ›   |›  expr '?' ' :' { zend_do_jmp_set(&$1, &$2, &$3 TSRMLS_CC); } 
835 ›   ›   expr     { zend_do_jmp_set_else(&$$ &$5, 12,如果你喜歡,可以自己動手,重新定義 ?: 的語法糖。遵循BNF文法規則,使用bison解析,有興趣可以自行Google相關知識,繼續深入了解。

從vld的opcode可以知道,執行了zend_do_jmp_set_else,程式碼在Zend/zend_compile.c 可以知道,執行了zend_do_jmp_set_else,程式碼在Zend/zend_compile.c 中: 

~~~.java 
void zend_do_jmp_set_else(znode zdek, constnok; colon_token TSRMLS_DC) 

›   zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC); 
  ›SET_NODE(opline-ken type == IS_TMP_VAR) { 
›​​   ›   if (false_value->op_type == IS_VAR || false_value->op_type == IS_CV) { 
›​​   ›   ›   CG(active_op_array)->opcodes[›mp_token-›   CG(active_op_array)->opcodes[›mp_token-›   CG(active_op_array)->opcodes[›mp_token-1.  ›   CG (active_op_array)->opcodes[jmp_token->u.op.opline_num].result_type = IS_VAR; 
›   ›   ›   opline->opcode = ZEND_QM_ASSIGN_VAR; 
›   ›   } else { 
›   ›   ›   opline->opcode = ZEND_QM_ASSIGN; 
›   ›   } 
›   } else { 
›​​›_ 
›   opline->extended_value = 0; 
›   SET_NODE(opline->op1, false_value); 
›   SET_UNUSED(opline->op2); 

›   GET_NODE(result, opline->result); 

›   GET_NODE(result, opline->result); 

› = get_next_op_number(CG(active_op_array)); 

›   DEC_BPC(CG(active_op_array)); 

~~~ 

#VA#3.接著讀代碼呢?下面說下PHP的opcode。

PHP5.6有167個opcode,意味著可以執行167種不同的計算操作,官方文件看這裡 

PHP內部使用_zend_op 這個結構體來表示opcode, vim Zend/zend_compile.h +111 

    111 struct _zend_op { 
    112 ›››. opcode_ ; 
    114 ›   znode_op op2; 
    115 ›   znode_op result; 
    116 ›   ulong extended_value; 
    117 ›   uint lineno; 
    118 ›   zend_uchar opcode 20 ›   zend_uchar op2_type; 
    121 ›   zend_uchar result_type; 
    122 } 

PHP 7.06略有不同,而主要差異在針對位元系統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_SETLVAp 3 { 
    4944 ›   USE_OPLINE 
    4945 ›   zend_free_op free_op1; 
    4946 ›   zval *value, *ret; 
    4947 
    4948 ›   SAVE_OPLINE
    R(BP_VAR_R); 
    4950 
    4951 ›   if (i_zend_is_true(value)) { 
  ›› 4952_is_true(value)) { 
  › 4952   ( OP1_TYPE == IS_VAR || OP1_TYPE == IS_CV) { 
    第4953章 
    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, 值); 
    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 ›   ›  
    4963 ›   ›   ›   } 
    4964 ›   ›   } 
 
    4966 #if DEBUG_ZEND>=2 
    4967 ›   ›   printf("有條件跳到 %dn", 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_execute.h`(這個檔案可以有45000多行哦),它不是手動生成的,而是由`zend_vm_gen.php`這個PHP腳本解析`zend_vm_def . h`後產生子彈(吧,先有雞還是先有有PHP哪來的這個腳本?),猜測這個是升級產物,早期php版本應該不會用這個。 `CONST|TMP|VAR|CV` 最終會產生不同類型的,但功能一致的處理函數: 

    static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_ARCODED_JMP_SET_VAR_SPEC_CONST_HANDLER(ZEND_ MP_SET_VAR_SPEC_TMP_HAN DL ER(ZEND_OPCODE_HANDLER_ARGS) 
    static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_VAR_HANDLER( ZEND_OPCODE_LD.
    static int ZEND_FASTCALL  ZEND_JMP_SET_VAR_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 

目的是為了在編譯時間確定後,提升運行期的性能。有些垃圾程式碼(外觀無用),不用擔心,C的編譯器會進一步最佳化處理。
## 4.

講到這裡,我們知道操作碼怎麼和處理程序回答了。說了,解析完成之後,將包含所有操作碼的大內存(說鍊錶可能更準確),從上面看到我們的個代碼可以看出,之後每個處理程序都執行,都會調用ZEND_VM_NEXT_OPCODE (),取出下一個操作,繼續執行,直到最後退出,循環的程式碼vim Zend/zend_vm_execute.h +337: 

~~~.java 
ZEND_API voidexecute_ex(zend_execute_dataexecL_data TSRMLS_DC) TSRM.  zend_booloriginal_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 月 
›   } 

›   LOAD_REGS(); 
›   LOAD_OPLINE(); 

›   while (1) { 
     out)) { 
›​​   ›   ›   zend_timeout(0) ; 
›   ›   } 
#endif 

›   ›   if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > ›0) { 🠎  ›   ›   ›   狀況1 : 
› ›   ›   ›   ›   EG(in_execution) = original_in_execution; 
›   ›   › 2: 
›   ›   ›   ›   ›   前往zend_vm_enter; 
›   ›   ›   ›   ››   ›   ›   ›  ›   ›   ›   狀況3: 
›   ›   ›   ›   ›  execute_data = EG(current_execute_   ›   ›   ›   預設值: 
›   ›   ›   ›   ›   中斷; 
›   ›   } 

›   } 
›   zend_error_noreturn(E_ERROR, "到達了不應該發生的主循環結束"); 

~~~  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(),返回0,繼續循環。

> 註:例如yield協程是個例外,它會回傳1,直接回到出循環。以後有機會我們再單獨對yield做分析。

希望你看完上面的內容,對PHP Zend引擎的解析過程有詳細的了解了,下面我們基於原理的分析,再簡單聊聊PHP的優化。 

## 5. PHP最佳化注意事項 

### 5.1 echo 輸出 

                 回顯 $foo 。 $?                fetch         ext return 操作數 
    ------- ---- ---------------------------------------------- ---------------------------- 
       2     0  E >              !0, 'foo'
3     1        ASSIGN                          !1, 'bar' 
       4     2        CONCAT                   ~2      !0, !1 
                                                                                          2-    5; sop:     0; eop: 4; out1:  -2 
    path #1: 0, 

ZEND_CONCAT 連接$a和$b的值,儲存到臨時變數~2中,然後echo 出來。這個過程中涉及要分配一塊內存,用於臨時變量,用完後還要釋放,還需要呼叫拼接函數,執行拼接過程。

如果換成這樣寫: 

            $foo = 'foo'; 
   

對應的opcode: 

    number of ops:  5
    compiled vars:  !0 = $foo, !1 = $bar 
    line     #* E I O op              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範例: 

           opcode: 

    number of ops:  6 
    compiled vars :  none 
    line     #* E I O op                   
    ------------------------------------- -------------------------------------------------- 
       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                  fetch         ext  return operands 
    ------------------- -------------------------------------------------- ---------------- 
       2     0  E >   DECLARE_CONST                     'FOO', 'foo' 
       3     1               ~0      'FOO' 
             2                                    ~0 
                                         
### 5.3 動態函數的代價 

            function foo() { } 
   ops:  3 
    compiled vars:  none 
    line     #* E I O op          ext  return  operands 
    ---------------------------------------------- --------------------------------------- 
       2     0  E >   NOP 
       3                            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              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 類別的延遲宣告的代價 

還是先看程式碼: 

          對應opcode: 

    number of ops:  4 
    compiled名詞  none 
    line     #* E I O op                  
    ------------------------------------------------ ------------------------------------------------- 
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              ext  return  operands 
    ------------------------------- -------------------------------------------------- ---- 
       2     0  E >   FETCH_CLASS                   'Bar' 
1        DECLARE_INHERITED_CLASS                     x103d58020', 'foo' 
       3     2        NOP 
                                    地動態語言,會把類別的聲明推遲到運行時,如果你不注意,就很可能踩到這個雷。 

所以在我們了解Zend VM原理後,就更應該注意少用動態特性,可有可無的時候,就一定不要用。 

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn