PHP7與PHP5在編譯執行上的區別
在PHP7之前的版本,PHP代碼在語法解析階段直接生成了ZendVM指令(也就是opline指令,後面會聊一下opline指令的~),這使得編譯器與執行器耦合在一起。這個模式的壞處就是,當我們如果要換一個VM時候,就需要修改語法解析規則,或者如果PHP的語法規則變了,(例如php中訪問對象使用->來訪問,想換成成java中 . 來訪問),我們也需要去修改語法解析規則。
PHP7加入了抽象語法樹,首先將PHP代碼解析成抽象語法樹,然後將抽象語法樹編譯成爲ZendVM指令。抽象語法樹的加入,使得PHP的編譯器與執行器很好的隔離開來了,編譯器不需要關心指令的生成規則,而執行器同樣不關心指令的語法規則是什麼樣子的,執行器根據自己的規則將抽象語法樹編譯成對應的指令。
編譯型語言和解析型語言的區別
編譯型語言 C語言
解析性語言 php
解析型語言與實際計算機之間多了一層解析器,屏蔽了不同平臺之間機器語言的差異,由解析器去處理不同平臺之間的差異,實現了跨平臺運行。但是帶來的代價是運行效率低,與編譯性語言直接執行機器指令想比,多了一層解析器的工作。
抽象語法樹(AST)
詞法解析,語法解析
PHP使用re2c、bison完成這個階段的工作:
- re2c: 詞法分析器,將輸入分割爲一個個有意義的詞塊,稱爲token
- yacc: 語法分析器,確定詞法分析器分割出的token是如何彼此關聯的
|
詞法分析器將上面的語句分解爲這些token:$a、=、2、+、3、- 、6,接着語法分析器確定了2+3-6
是一個表達式,而這個表達式被賦值給了a
語法分析樹
PHP的抽象語法樹定義
|
PHP抽象語法樹拆分邏輯比較難理解,我給出一個例子,我們看下最終生成的語法樹。
|
Zend虛擬機
Zend虛擬機就是PHP語言的解析器,負責PHP代碼的解析,執行。
ZendVM對於計算機而言就是普通二進制可執行程序,是編譯好的機器指令,而PHP代碼被編譯成ZendVM可識別的指令,而不是機器指令,然後ZendVM來執行,最終變爲機器指令。
ZendVM由以下部分組成:
Opline指令
opline是ZendVM定義的執行指令,每條指令的編碼都是opcode。
struct _zend_op {
const void *handler; //指令執行handler
znode_op op1; //操作數1
znode_op op2; //操作數2
znode_op result; //返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode; //opcode指令
zend_uchar op1_type; //操作數1類型
zend_uchar op2_type; //操作數2類型
zend_uchar result_type; //返回值類型
};
//opcode 定義
#define ZEND_NOP 0
#define ZEND_ADD 1
#define ZEND_SUB 2
#define ZEND_MUL 3
#define ZEND_DIV 4
#define ZEND_MOD 5
#define ZEND_SL 6
#define ZEND_SR 7
#define ZEND_CONCAT 8
#define ZEND_BW_OR 9
#define ZEND_BW_AND 10
#define ZEND_BW_XOR 11
舉個例子:例如賦值操作 $a = 123,操作數1用來告訴VM 變量$a的位置,操作數2用來保存變量123的位置,執行的時候,ZendVM從操作數1與2獲取信息,然後進行對應動作(此時opcode指令爲 ZEND_ADD)opline指令描述其實就是:對什麼數據,做什麼處理!
Zend_op_array
opline 是編譯生成的單條指令,所有的指令組合生成了zend_op_array
struct _zend_op_array {
/* Common elements */
.... 以上省略
uint32_t *refcount;
uint32_t this_var;
uint32_t last;
zend_op *opcodes; //這裏就是指令集合,是一個數組,執行器執行時從該數組的第一條指令開始,直到最後
int last_var;
uint32_t T;
zend_string **vars;
....以上省略
};
zend_execute_data
是執行過程中最核心的一個結構,每次函數的調用、include/require、eval等都會生成一個新的結構,它表示當前的作用域、代碼的執行位置以及局部變量的分配等等。Zend_execute_data
#define EX(element) ((execute_data)->element)
//zend_compile.h
struct _zend_execute_data {
const zend_op *opline; //指向當前執行的opcode,初始時指向zend_op_array起始位置
zend_execute_data *call; /* current call */
zval *return_value; //返回值指針
zend_function *func; //當前執行的函數(非函數調用時爲空)
zval This; //這個值並不僅僅是面向對象的this,還有另外兩個值也通過這個記錄:call_info + num_args,分別存在zval.u1.reserved、zval.u2.num_args
zend_class_entry *called_scope; //當前call的類
zend_execute_data *prev_execute_data; //函數調用時指向調用位置作用空間
zend_array *symbol_table; //全局變量符號表
#if ZEND_EX_USE_RUN_TIME_CACHE
void **run_time_cache; /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
zval *literals; //字面量數組,與func.op_array->literals相同
#endif
};
Zend_execute_data 與Zend_op_array的關係
執行器
zend執行opcode的簡略過程
- step1: 爲當前作用域分配一塊內存,充當運行棧,zend_execute_data結構、所有局部變量、中間變量等等都在此內存上分配
- step2: 初始化全局變量符號表,然後將全局執行位置指針EG(current_execute_data)指向step1新分配的zend_execute_data,然後將zend_execute_data.opline指向op_array的起始位置
- step3: 從EX(opline)開始調用各opcode的C處理handler(即_zend_op.handler),每執行完一條opcode將
EX(opline)++
繼續執行下一條,直到執行完全部opcode,函數/類成員方法調用、if的執行過程: - step3.1: 如果是函數調用,則首先從EG(function_table)中根據function_name取出此function對應的編譯完成的zend_op_array,然後像step1一樣新分配一個zend_execute_data結構,將EG(current_execute_data)賦值給新結構的
prev_execute_data
,再將EG(current_execute_data)指向新的zend_execute_data,最後從新的zend_execute_data.opline
開始執行,切換到函數內部,函數執行完以後將EG(current_execute_data)重新指向EX(prev_execute_data),釋放分配的運行棧,銷燬局部變量,繼續從原來函數調用的位置執行 - 全部opcode執行完成後將step1分配的內存釋放,這個過程會將所有的局部變量"銷燬",執行階段結束