php7 passthru,认识PHP 7虚拟机
PHP : 一門解釋型語言
PHP被稱為腳本語言或解釋型語言。為何?
PHP語言沒有被直接編譯為機器指令,而是編譯為一種中間代碼的形式,很顯然它無法直接在CPU上執行。
所以PHP的執行需要在進程級虛擬機上(見Virtual machine中的Process virtual machines,下文簡稱虛擬機)。
PHP語言,包括其他的解釋型語言,其實是一個跨平臺的被設計用來執行抽象指令的程序。PHP主要用于解決WEB開發相關的問題。
諸如Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript等編程語言所編寫的程序,都需要在虛擬機上執行。虛擬機可以通過JIT編譯技術將一部分虛擬機指令編譯為機器指令以提高性能。鳥哥已經在進行PHP加入JIT支持的開發了。
使用解釋型語言的優點:
代碼編寫簡單,能夠快速開發
自動的內存管理
抽象的數據類型,程序可移植性高
缺點:
無法直接地進行內存管理和使用進程資源
比編譯為機器指令的語言速度慢:通常需要更多的CPU周期來完成相同的任務(JIT試圖縮小差距,但永遠不能完全消除)
抽象了太多東西,以至于當程序出問題時,許多程序員難以解釋其根本原因
最后一條缺點是作者之所以寫這篇文章的原因,作者覺得程序員應該去了解一些底層的東西。
作者希望能夠通過這篇文章向讀者講明白PHP是如何運行的。本文所提到的關于PHP虛擬機的知識同樣可以應用于其他解釋型語言。通常,不同虛擬機實現上的最大不同點在于:是否使用JIT、并行的虛擬機指令(一般使用多線程實現,PHP沒有使用這一技術)、內存管理/垃圾回收算法。
Zend虛擬機分為兩大部分:
編譯:將PHP代碼轉換為虛擬機指令(OPCode)
執行:執行生成的虛擬機指令
本文不會涉及到編譯部分,主要關注Zend虛擬機的執行引擎。PHP7版本的執行引擎做了一部分重構,使得PHP代碼的執行堆棧更加簡單清晰,性能也得到了一些提升。
本文以PHP 7.0.7為示例。
OPCode
維基百科對于OPCode的解釋:
Opcodes can also be found in so-called byte codes and other representations intended for a software interpreter rather than a hardware device. These software based instruction sets often employ slightly higher-level data types and operations than most hardware counterparts, but are nevertheless constructed along similar lines.
OPCode與ByteCode在概念上是不同的。
我的個人理解:OPCode作為一條指令,表明要怎么做,而ByteCode由一序列的OPCode/數據組成,表明要做什么。以一個加法為例子,OPCode是告訴執行引擎將參數1和參數2相加,而ByteCode則告訴執行引擎將45和56相加。
在PHP中,Zend/zend_vm_opcodes.h源碼文件列出了所有支持的OPCode。通常,每個OPCode的名字都描述了其含義,比如:
ZEND_ADD:對兩個操作數執行加法操作
ZEND_NEW:創建一個對象
ZEND_FETCH_DIM_R:讀取操作數中某個維度的值,比如執行echo $foo[0]語句時,需要獲取$foo數組索引為0的值
OPCode以zend_op結構體表示:
struct _zend_op {
const void *handler; /* 執行該OPCode的C函數 */
znode_op op1; /* 操作數1 */
znode_op op2; /* 操作數2 */
znode_op result; /* 結果 */
uint32_t extended_value; /* 額外的信息 */
uint32_t lineno; /* 該OPCode對應PHP源碼所在的行 */
zend_uchar opcode; /* OPCode對應的數值 */
zend_uchar op1_type; /* 操作數1類型 */
zend_uchar op2_type; /* 操作數2類型 */
zend_uchar result_type; /* 結果類型 */
};
每一條OPcode都以相同的方式執行:OPCode有其對應的C函數,執行該C函數時,可能會用到0、1或2個操作數(op1,op2),最后將結果存儲在result中,可能還會有一些額外的信息存儲在extended_value。
看下ZEND_ADD的OPCode長什么樣子,在Zend/zend_vm_def.h源碼文件中:
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
{
USE_OPLINE
zend_free_op free_op1, free_op2;
zval *op1, *op2, *result;
op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
result = EX_VAR(opline->result.var);
fast_long_add_function(result, op1, op2);
ZEND_VM_NEXT_OPCODE();
} else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
result = EX_VAR(opline->result.var);
ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
ZEND_VM_NEXT_OPCODE();
}
} else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
result = EX_VAR(opline->result.var);
ZVAL_DOUBLE(result, Z_DVAL_P(op1) + Z_DVAL_P(op2));
ZEND_VM_NEXT_OPCODE();
} else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
result = EX_VAR(opline->result.var);
ZVAL_DOUBLE(result, Z_DVAL_P(op1) + ((double)Z_LVAL_P(op2)));
ZEND_VM_NEXT_OPCODE();
}
}
SAVE_OPLINE();
if (OP1_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) {
op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
}
if (OP2_TYPE == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) {
op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
}
add_function(EX_VAR(opline->result.var), op1, op2);
FREE_OP1();
FREE_OP2();
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
可以看出這其實不是一個合法的C代碼,可以把它看成代碼模板。稍微解讀下這個代碼模板:1 就是在Zend/zend_vm_opcodes.h中define定義的ZEND_ADD的值;ZEND_ADD接收兩個操作數,如果兩個操作數都為IS_LONG類型,那么就調用fast_long_add_function(該函數內部使用匯編實現加法操作);如果兩個操作數,都為IS_DOUBLE類型或者1個是IS_DOUBLE類型,另1個是IS_LONG類型,那么就直接執行double的加法操作;如果存在1個操作數不是IS_LONG或IS_DOUBLE類型,那么就調用add_function(比如兩個數組做加法操作);最后檢查是否有異常接著執行下一條OPCode。
在Zend/zend_vm_def.h源碼文件中的內容其實是OPCode的代碼模板,在該源文件的開頭處可以看到這樣一段注釋:
/* If you change this file, please regenerate the zend_vm_execute.h and
* zend_vm_opcodes.h files by running:
* php zend_vm_gen.php
*/
說明zend_vm_execute.h和zend_vm_opcodes.h,實際上包括zend_vm_opcodes.c中的C代碼正是從Zend/zend_vm_def.h的代碼模板生成的。
操作數類型
每個OPCode最多使用兩個操作數:op1和op2。每個操作數代表著OPCode的“形參”。例如ZEND_ASSIGN OPCode將op2的值賦值給op1代表的PHP變量,而其result則沒有使用到。
操作數的類型(與PHP變量的類型不同)決定了其含義以及使用方式:
IS_CV:Compiled Variable,說明該操作數是一個PHP變量
IS_TMP_VAR :虛擬機使用的臨時內部PHP變量,不能夠在不同OPCode中復用(復用的這一點我并不清楚,還沒去研究過)
IS_VAR:虛擬機使用的內部PHP變量,能夠在不同OPCode中復用(復用的這一點我并不清楚,還沒去研究過)
IS_CONST:代表一個常量值
IS_UNUSED:該操作數沒有任何意義,忽略該操作數
操作數的類型對性能優化和內存管理很重要。當一個OPCode的Handler需要讀寫操作數時,會根據操作數的類型通過不同的方式讀寫。
以加法例子,說明操作數類型:
$a + $b; // IS_CV + IS_CV
1 + $a; // IS_CONST + IS_CV
$$b + 3 // IS_VAR + IS_CONST
!$a + 3; // IS_TMP_VAR + IS_CONST
OPCode Handler
我們已經知道每個OPCode Handler最多接收2個操作數,并且會根據操作數的類型讀寫操作數的值。如果在Handler中,通過switch判斷類型,然后再讀寫操作數的值,那么對性能會有很大損耗,因為存在太多的分支判斷了(Why is it good to avoid instruction branching where possible?),如下面的偽代碼所示:
int ZEND_ADD(zend_op *op1, zend_op *op2)
{
void *op1_value;
void *op2_value;
switch (op1->type) {
case IS_CV:
op1_value = read_op_as_a_cv(op1);
break;
case IS_VAR:
op1_value = read_op_as_a_var(op1);
break;
case IS_CONST:
op1_value = read_op_as_a_const(op1);
break;
case IS_TMP_VAR:
op1_value = read_op_as_a_tmp(op1);
break;
case IS_UNUSED:
op1_value = NULL;
break;
}
/* ... same thing to do for op2 .../
/* do something with op1_value and op2_value (perform a math addition ?) */
}
要知道OPCode Handler在PHP執行過程中是會被調用成千上萬次的,所以在Handler中對op1、op2做類型判斷,對性能并不好。
重新看下ZEND_ADD的代碼模板:
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
這說明ZEND_ADD接收op1和op2為CONST或TMPVAR或CV類型的操作數。
前面已經提到zend_vm_execute.h和zend_vm_opcodes.h中的C代碼是從Zend/zend_vm_def.h的代碼模板生成的。通過查看zend_vm_execute.h,可以看到每個OPCode對應的Handler(C函數),大部分OPCode會對應多個Handler。以ZEND_ADD為例:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CV_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_TMPVAR_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_TMPVAR_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_TMPVAR_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
ZEND_ADD的op1和op2的類型都有3種,所以一共生成了9個Handler,每個Handler的命名規范:ZEND_{OPCODE-NAME}_SPEC_{OP1-TYPE}_{OP2-TYPE}_HANDLER()。在編譯階段,操作數的類型是已知的,也就確定了每個編譯出來的OPCode對應的Handler了。
那么這些Handler之間有什么不同呢?最大的不同應該就是獲取操作數的方式:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *op1, *op2, *result;
op1 = EX_CONSTANT(opline->op1);
op2 = EX_CONSTANT(opline->op2);
if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
/* 省略 */
} else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
/* 省略 */
}
SAVE_OPLINE();
if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) { //
op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
}
if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) { //
op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
}
add_function(EX_VAR(opline->result.var), op1, op2);
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *op1, *op2, *result;
op1 = EX_CONSTANT(opline->op1);
op2 = _get_zval_ptr_cv_undef(execute_data, opline->op2.var); //
if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
/* 省略 */
} else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
/* 省略 */
}
SAVE_OPLINE();
if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) { //
op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
}
if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) { //
op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
}
add_function(EX_VAR(opline->result.var), op1, op2);
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
OPArray
OPArray是指一個包含許多要被順序執行的OPCode的數組,如下圖:
OPArray由結構體_zend_op_array表示:
struct _zend_op_array {
/* Common elements */
/* 省略 */
/* END of common elements */
/* 省略 */
zend_op *opcodes; //
/* 省略 */
};
在PHP中,每個PHP用戶函數或者PHP腳本、傳遞給eval()的參數,會被編譯為一個OPArray。
OPArray中包含了許多靜態的信息,能夠幫助執行引擎更高效地執行PHP代碼。部分重要的信息如下:
當前腳本的文件名,OPArray對應的PHP代碼在腳本中起始和終止的行號
/**的代碼注釋信息
refcount引用計數,OPArray是可共享的
try-catch-finally的跳轉信息
break-continue的跳轉信息
當前作用域所有PHP變量的名稱
函數中用到的靜態變量
literals(字面量),編譯階段已知的值,例如字符串“foo”,或者整數42
運行時緩存槽,引擎會緩存一些后續執行需要用到的東西
一個簡單的例子:
$a = 8;
$b = 'foo';
echo $a + $b;
OPArray中的部分成員其內容如下:
OPArray包含的信息越多,即在編譯期間盡量的將已知的信息計算好存儲到OPArray中,執行引擎就能夠更高效地執行。我們可以看到每個字面量都已經被編譯為zval并存儲到literals數組中(你可能發現這里多了一個整型值1,其實這是用于ZEND_RETURN OPCode的,PHP文件的OPArray默認會返回1,但函數的OPArray默認返回null)。OPArray所使用到的PHP變量的名字信息也被編譯為zend_string存儲到vars數組中,編譯后的OPCode則存儲到opcodes數組中。
OPCode的執行
OPCode的執行是通過一個while循環去做的:
//刪除了預處理語句
ZEND_API void execute_ex(zend_execute_data *ex)
{
DCL_OPLINE
const zend_op *orig_opline = opline;
zend_execute_data *orig_execute_data = execute_data;
execute_data = ex;
LOAD_OPLINE();
while (1) {
((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); //執行OPCode對應的C函數
if (UNEXPECTED(!OPLINE)) { //當前OPArray執行完
execute_data = orig_execute_data;
opline = orig_opline;
return;
}
}
zend_error_noreturn(E_CORE_ERROR, "Arrived at end of main loop which shouldn't happen");
}
那么是如何切換到下一個OPCode去執行的呢?每個OPCode的Handler中都會調用到一個宏:
#define ZEND_VM_NEXT_OPCODE_EX(check_exception, skip) \
CHECK_SYMBOL_TABLES() \
if (check_exception) { \
OPLINE = EX(opline) + (skip); \
} else { \
OPLINE = opline + (skip); \
} \
ZEND_VM_CONTINUE()
該宏會把當前的opline+skip(skip通常是1),將opline指向下一條OPCode。opline是一個全局變量,指向當前執行的OPCode。
額外的一些東西
編譯器優化
在Zend/zend_vm_execute.h中,會看到如下奇怪的代碼:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_ARRAY_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
/* 省略 */
if (IS_CONST == IS_UNUSED) {
ZEND_VM_NEXT_OPCODE();
#if 0 || (IS_CONST != IS_UNUSED)
} else {
ZEND_VM_TAIL_CALL(ZEND_ADD_ARRAY_ELEMENT_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
#endif }
}
你可能會對if (IS_CONST == IS_UNUSED)和#if 0 || (IS_CONST != IS_UNUSED)感到奇怪??聪缕鋵哪0宕a:
ZEND_VM_HANDLER(71, ZEND_INIT_ARRAY, CONST|TMP|VAR|UNUSED|CV, CONST|TMPVAR|UNUSED|CV)
{
zval *array;
uint32_t size;
USE_OPLINE
array = EX_VAR(opline->result.var);
if (OP1_TYPE != IS_UNUSED) {
size = opline->extended_value >> ZEND_ARRAY_SIZE_SHIFT;
} else {
size = 0;
}
ZVAL_NEW_ARR(array);
zend_hash_init(Z_ARRVAL_P(array), size, NULL, ZVAL_PTR_DTOR, 0);
if (OP1_TYPE != IS_UNUSED) {
/* Explicitly initialize array as not-packed if flag is set */
if (opline->extended_value & ZEND_ARRAY_NOT_PACKED) {
zend_hash_real_init(Z_ARRVAL_P(array), 0);
}
}
if (OP1_TYPE == IS_UNUSED) {
ZEND_VM_NEXT_OPCODE();
#if !defined(ZEND_VM_SPEC) || (OP1_TYPE != IS_UNUSED) } else {
ZEND_VM_DISPATCH_TO_HANDLER(ZEND_ADD_ARRAY_ELEMENT);
#endif }
}
php zend_vm_gen.php在生成zend_vm_execute.h時,會把OP1_TYPE替換為op1的類型,從而生成這樣子的代碼:if (IS_CONST == IS_UNUSED),但C編譯器會把這些代碼優化掉。
自定義Zend執行引擎的生成
zend_vm_gen.php支持傳入參數--without-specializer,當使用該參數時,每個OPCode只會生成一個與之對應的Handler,該Handler中會對操作數做類型判斷,然后再對操作數進行讀寫。
另一個參數是--with-vm-kind=CALL|SWITCH|GOTO,CALL是默認參數。
前面已提到執行引擎是通過一個while循環執行OPCode,每個OPCode中將opline增加1(通常情況下),然后回到while循環中,繼續執行下一個OPCode,直到遇到ZEND_RETURN。
如果使用GOTO執行策略:
/* GOTO策略下,execute_ex是一個超大的函數 */
ZEND_API void execute_ex(zend_execute_data *ex)
{
/* 省略 */
while (1) {
/* 省略 */
goto *(void**)(OPLINE->handler);
/* 省略 */
}
/* 省略 */
}
這里的goto并沒有直接使用符號名,其實是goto一個特殊的用法:Labels as Values。
執行引擎中的跳轉
當PHP腳本中出現if語句時,是如何跳轉到相應的OPCode然后繼續執行的?看下面簡單的例子:
$a = 8;
if ($a == 9) {
echo "foo";
} else {
echo "bar";
}
number of ops: 7
compiled vars: !0 = $a
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 8
3 1 IS_EQUAL ~2 !0, 9
2 > JMPZ ~2, ->5
4 3 > ECHO 'foo'
4 > JMP ->6
6 5 > ECHO 'bar'
6 > > RETURN 1
當$a != 9時,JMPZ會使當前執行跳轉到第5個OPCode,否則JMP會使當前執行跳轉到第6個OPCode。其實就是對當前的opline賦值為跳轉目標OPCode的地址。
一些性能Tips
這部分內容將展示如何通過查看生成的OPCode優化PHP代碼。
echo a concatenation
示例代碼:
$foo = 'foo';
$bar = 'bar';
echo $foo . $bar;
OPArray:
number of ops: 5
compiled vars: !0 = $foo, !1 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'foo'
3 1 ASSIGN !1, 'bar'
5 2 CONCAT ~4 !0, !1
3 ECHO ~4
4 > RETURN 1
$a和$b的值會被ZEND_CONCAT連接后存儲到一個臨時變量~4中,然后再echo輸出。
CONCAT操作需要分配一塊臨時的內存,然后做內存拷貝,echo輸出后,又要回收這塊臨時內存。如果把代碼改為如下可消除CONCAT:
$foo = 'foo';
$bar = 'bar';
echo $foo , $bar;
OPArray:
number of ops: 5
compiled vars: !0 = $foo, !1 = $bar
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > ASSIGN !0, 'foo'
3 1 ASSIGN !1, 'bar'
5 2 ECHO !0
3 ECHO !1
4 > RETURN 1
define()和const
PHP 5.3引入了const關鍵字。
簡單地說:
define()是一個函數調用
conast是關鍵字,不會產生函數調用,要比define()輕量許多
define('FOO', 'foo');
echo FOO;
number of ops: 7
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > INIT_FCALL 'define'
1 SEND_VAL 'FOO'
2 SEND_VAL 'foo'
3 DO_ICALL
3 4 FETCH_CONSTANT ~1 'FOO'
5 ECHO ~1
6 > RETURN 1
如果使用const:
const FOO = 'foo';
echo FOO;
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
3 > RETURN 1
然而const在使用上有一些限制:
const關鍵字定義常量必須處于最頂端的作用區域,這就意味著不能在函數內,循環內以及if語句之內用const 來定義常量
const的操作數必須為IS_CONST類型
動態函數調用
盡量不要使用動態的函數名去調用函數:
function foo() { }
foo();
number of ops: 4
compiled vars: none
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 INIT_FCALL 'foo'
2 DO_UCALL
3 > RETURN 1
NOP表示不做任何操作,只是將當前opline指向下一條OPCode,編譯器產生這條指令是由于歷史原因。為何到PHP7還不移除它呢= =
看看使用動態的函數名去調用函數:
function foo() { }
$a = 'foo';
$a();
number of ops: 5
compiled vars: !0 = $a
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
2 0 E > NOP
3 1 ASSIGN !0, 'foo'
4 2 INIT_DYNAMIC_CALL !0
3 DO_FCALL 0
4 > RETURN 1
不同點在于INIT_FCALL和INIT_DYNAMIC_CALL,看下兩個函數的源碼:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *fname = EX_CONSTANT(opline->op2);
zval *func;
zend_function *fbc;
zend_execute_data *call;
fbc = CACHED_PTR(Z_CACHE_SLOT_P(fname)); /* 看下是否已經在緩存中了 */
if (UNEXPECTED(fbc == NULL)) {
func = zend_hash_find(EG(function_table), Z_STR_P(fname)); /* 根據函數名查找函數 */
if (UNEXPECTED(func == NULL)) {
SAVE_OPLINE();
zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(fname));
HANDLE_EXCEPTION();
}
fbc = Z_FUNC_P(func);
CACHE_PTR(Z_CACHE_SLOT_P(fname), fbc); /* 緩存查找結果 */
}
call = zend_vm_stack_push_call_frame_ex(
opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
fbc, opline->extended_value, NULL, NULL);
call->prev_execute_data = EX(call);
EX(call) = call;
ZEND_VM_NEXT_OPCODE();
}
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_DYNAMIC_CALL_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
/* 200多行代碼,就不貼出來了,會根據CV的類型(字符串、對象、數組)做不同的函數查找 */
}
很顯然INIT_FCALL相比INIT_DYNAMIC_CALL要輕量許多。
類的延遲綁定
簡單地說,類A繼承類B,類B最好先于類A被定義。
class Bar { }
class Foo extends Bar { }
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
3 > RETURN 1
從生成的OPCode可以看出,上述PHP代碼在運行時,執行引擎不需要做任何操作。類的定義是比較耗性能的工作,例如解析類的繼承關系,將父類的方法/屬性添加進來,但編譯器已經做完了這些繁重的工作。
如果類A先于類B被定義:
class Foo extends Bar { }
class Bar { }
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%2Fhome%2Froketyyang%2Ftest.php0x7fb192b7101f', 'foo'
3 2 NOP
3 > RETURN 1
這里定義了Foo繼承自Bar,但當編譯器讀取到Foo的定義時,編譯器并不知道任何關于Bar的情況,所以編譯器就生成相應的OPCode,使其定義延遲到執行時。在一些其他的動態類型的語言中,可能會產生錯誤:Parse error : class not found。
除了類的延遲綁定,像接口、traits都存在延遲綁定耗性能的問題。
對于定位PHP性能問題,通常都是先用xhprof或xdebug profile進行定位,需要通過查看OPCode定位性能問題的場景還是比較少的。
總結
希望通過這篇文章,能讓你了解到PHP虛擬機大致是如何工作的。具體opcode的執行,以及函數調用涉及到的上下文切換,有許多細節性的東西,限于本文篇幅,在另一篇文章:PHP 7 中函數調用的實現進行講解。
總結
以上是生活随笔為你收集整理的php7 passthru,认识PHP 7虚拟机的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java工程引用其他工程,Android
- 下一篇: java调试生命周期,一种基于JAVA的