## 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原理後,就更應該注意少用動態特性,可有可無的時候,就一定不要用。