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