本文內容大部分翻譯自Getting into the Zend Execution engine (PHP 5),並做了一些調整,原文基於PHP 5,本文基於PHP 7。
PHP稱為腳本語言或解釋型語言。為何? PHP語言並沒有被直接編譯為機器指令,而是編譯成一種中間程式碼的形式,很顯然它無法直接在CPU上執行。所以PHP的執行需要在進程級虛擬機器上(請參閱Virtual machine中的Process virtual machines,下文簡稱虛擬機器)。
PHP語言,包括其他的解釋型語言,其實是一個跨平台的被設計用來執行抽象指令的程式。 PHP主要用於解決WEB開發相關的問題。
諸如Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript等程式語言所編寫的程序,都需要在虛擬機器上執行。虛擬機器可以透過JIT編譯技術將一部分虛擬機器指令編譯為機器指令以提高效能。鳥哥已經在進行PHP加入JIT支援的開發了。
推薦教學:《PHP教學》
使用解譯型語言的優點:
程式碼編寫簡單,能夠快速開發
自動的記憶體管理
抽象的資料類型,程式可移植性高
缺點:
無法直接地進行記憶體管理和使用進程資源
比編譯為機器指令的語言速度慢:通常需要更多的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相加。
參考:Difference between Opcode and Bytecode和Difference between: Opcode, byte code, mnemonics, machine code and assembly
在PHP中,Zend/zend_vm_opcodes。文件列出了所有支援的OPCode。通常,每個OPCode的名字都描述了其意義,例如:
語句時,需要取得$foo陣列索引為0的值
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原始碼檔案中:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">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();
}</pre><div class="contentsignin">登入後複製</div></div>
<p>可以看出这其实不是一个合法的C代码,可以把它看成代码模板。稍微解读下这个代码模板:1 就是在<code>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最多接收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); //<-------- op2的获取方式与上面的CONST不同 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)) { //<-------- IS_CV == IS_CV && 也会被编译器优化掉 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是指一个包含许多要被顺序执行的OPCode的数组,如下图:
OPArray由结构体_zend_op_array表示:
struct _zend_op_array { /* Common elements */ /* 省略 */ /* END of common elements */ /* 省略 */ zend_op *opcodes; //<------ 存储着OPCode的数组 /* 省略 */ };
在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的执行是通过一个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)
感到奇怪。看下其对应的模板代码:
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_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的地址。
这部分内容将展示如何通过查看生成的OPCode优化PHP代码。
示例代码:
$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
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 中函数调用的实现进行讲解。
推荐相关文章:《linux系统教程》
以上是了解什麼是PHP7虛擬機的詳細內容。更多資訊請關注PHP中文網其他相關文章!