PHP被称为脚本语言或解释型语言。为何? PHP语言没有被直接编译为机器指令,而是编译为一种中间代码的形式,很显然它无法直接在CPU上执行。 所以PHP的执行需要在进程级虚拟机上(见Virtual machine中的Process virtual machines,下文简称虚拟机)。
PHP语言,包括其他的解释型语言,其实是一个跨平台的被设计用来执行抽象指令的程序。PHP主要用于解决WEB开发相关的问题。
诸如Java, Python, C#, Ruby, Pascal, Lua, Perl, Javascript等编程语言所编写的程序,都需要在虚拟机上执行。虚拟机可以通过JIT编译技术将一部分虚拟机指令编译为机器指令以提高性能。鸟哥已经在进行PHP加入JIT支持的开发了。
使用解释型语言的优点:
缺点:
作者希望能够通过这篇文章向读者讲明白PHP是如何运行的。本文所提到的关于PHP虚拟机的知识同样可以应用于其他解释型语言。通常,不同虚拟机实现上的最大不同点在于:是否使用JIT、并行的虚拟机指令(一般使用多线程实现,PHP没有使用这一技术)、内存管理/垃圾回收算法。
Zend虚拟机分为两大部分:
本文不会涉及到编译部分,主要关注Zend虚拟机的执行引擎。PHP7版本的执行引擎做了一部分重构,使得PHP代码的执行堆栈更加简单清晰,性能也得到了一些提升。
本文以PHP 7.0.7为示例。
维基百科对于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.h源码文件列出了所有支持的OPCode。通常,每个OPCode的名字都描述了其含义,比如:
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();
}
{
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_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变量的类型不同)决定了其含义以及使用方式:
操作数的类型对性能优化和内存管理很重要。当一个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)
ZENDADD的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();
}
ZENDADD的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();
}