大家好,欢迎来到IT知识分享网。
今天看到一篇不错的PHP底层运行的文章,里面详细介绍了zend引擎是如何执行php代码的,包括OPcode 、执行OPcode的循环 等内容
我把其中比较重要的信息贴在这里,以飨读者。
——————————————————–华丽的分割线———————————————————————-
PHP是解释性语言,所谓“解释型语言”就是指用这种语言写的程序不会被直接编译为本地机器语言,而是会被编译为一种中间形式(代码),很显然这种中间形式不可能直接在CPU上执行(因为CPU只能执行本地机器指令),但是这种中间形式可以在使用本地机器指令(如今大多是使用C语言)编写的软件上执行。
Zend软件虚拟机
PHP使用主要虚拟机可以分为两大部分,它们是紧密相连的:
- 编译栈(compile stack):识别PHP语言指令,把它们转换为中间形式
- 执行栈(execution stack):获取中间形式的代码指令并在引擎上执行,引擎是用C或者汇编编写成的
这篇文章不会谈论第一部分,而会专注于Zend虚拟机的executor。我们在这篇文章中以PHP5.6为例来讲解。
OPCode
OPCode也会出现在所谓的字节码(byte codes)中,或者是被软件解释器(而不是硬件设备)解释执行的指令的其他形式中。这些软件指令集通常会提供一些比对应的硬件指令集更高级(higher-level)的数据类型和操作,尽管它们每个指令执行的结果都是差不多的。
通常而言,OPCode的名称是自描述的,例如:
- ZEND_ADD :执行两个操作数的算术加法运算
- ZEND_NEW :创建一个对象(一个PHP对象)
- ZEND_EXIT :退出PHP执行
- ZEND_FETCH_DIM_W : 取一个操作数在某个维度(dimension)下的值,然后执行写入操作(译注:这里的“维度”指的一维数组,二维数组的“维度”,给数组中的某个元素赋值,或者是给字符串所在某个位置的字符赋值都会用到这个OPCode)
- 等等
PHP5.6有167个OPCode。因此我们可以说PHP5.6的虚拟机的executor可以执行167种不同的(计算)操作。
OPCode结构体
PHP内部使用zend_op这个结构体来表示OPCode:
struct _zend_op { opcode_handler_t handler; /* The true C function to run */ znode_op op1; /* operand 1 */ znode_op op2; /* operand 2 */ znode_op result; /* result */ ulong extended_value; /* additionnal little piece of information */ uint lineno; zend_uchar opcode; /* opcode number */ zend_uchar op1_type; /* operand 1 type */ zend_uchar op2_type; /* operand 2 type */ zend_uchar result_type; /* result type */ };
Zend VM的每个OPCode的工作方式都完全相同,他们大概包含下列东西:
- 一个handle:一个C函数,它指向OPCode对应的处理函数的地址,这个处理函数就是用于实现OPCode具体操作的,例如“add”,它就会执行一个基本的加法运算
- 操作数1 :opt1
- 操作数2 :opt2
这个函数运行后,它会后返回一个结果,有时也会返回一段信息
OPcode的handler
Zend VM的每个OPCode的工作方式都完全相同:它们都有一个handler,每个handler都可以使用0、1或者2个操作数:op1和op2,这个函数运行后,它会后返回一个结果,有时也会返回一段信息(extended_value)。
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV) { USE_OPLINE zend_free_op free_op1, free_op2; SAVE_OPLINE(); fast_add_function(&EX_T(opline->result.var).tmp_var, GET_OP1_ZVAL_PTR(BP_VAR_R), GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC); FREE_OP1(); FREE_OP2(); CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); }
你只需要注意其中你可以理解的行,上面的这段代码并不符合C语法(这一点等会会谈到)。尽管如此,这段代码还是很容易理解的。
正如前面所说,ZEND_ADD这个OPCode的handler中会调用fast_add_function()函数(一个存放在其他地方的C函数),这段代码中给这个函数传入了三个参数:result(结果)、op1和op2。从这段代码我们可以看出真正执行加法运算的代码是在fast_add_function()这个函数中,在此我们就不展示这个函数的代码了。
上面的handler代码的最后部分会调用CHECK_EXCEPTION()和ZEND_VM_NEXT_OPCODE()两个指令(译注:实际上是两个C的宏),我们先讲解一下后一个指令(instruction)。
一个超大的循环
当在编译PHP脚本时,脚本中的PHP语法会被转换为多个OPCode,一个接着一个。
这意味着PHP编译器会做一件事情:把PHP脚本转换为一个“OP数组(OP array)”,它是一个包含多个OPCode的数组。每个OPCode的handler都会以调用ZEND_VM_NEXT_OPCODE()结束,它会告诉executor提取(fetch)紧接着的下一个OPCode,然后执行它,这个过程会不断进行。
一个简单的示例
$a = 8;
$b = ‘foo’;
echo $a + $b;
这个简单的脚本会被编译成如下所示的OPArray (ASSIGN,ASSIGN,ADD,ECHO,RETURN)
compiled vars: !0 = $a, !1 = $b line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 2 0 > ASSIGN !0, 8 3 1 ASSIGN !1, 'foo' 4 2 ADD ~2 !0, !1 3 ECHO ~2 5 4 > RETURN 1
相信大家可以理解上面输出的OPCode,我在此简单说明下:
- 把8赋值(assign)给$a
- 把’foo’赋值给$b
- 把$a和$b中的值相加然后保存在一个临时变量“~2”中
- echo临时变量“~2”
- 返回(return)
你可能已经注意到一个最后一行的OPCode:RETURN,很显然:每个脚本都会以一个RETURN结束,如果不是这样的话:整个循环会无限执行下去,这显然是不合理的。
所以PHP编译器被设计为不管编译什么代码都会在编译出的OP数组的最后加一个RETURN的OPCode。这意味着如果编译一个空的PHP脚本(不包含任何代码),所产生的OPArray中也会包含一个唯一的OPCode:ZEND_RETURN。当它被加载到VM的执行分发循环时,它会执行这个OPCode的handler的代码,那就是让VM返回:空PHP脚本不会做其他任何事情。
OPArray
Zend VM的操作数类型
我们知道每个OPCode的handler最多可以使用两个操作数:op1和op2。每个操作数都表示一个OPCode的“参数(parameter)”。例如,ZEND_ASSIGN(赋值的OPcode)这个OPCode的第一个参数是你要赋值的PHP变量,第二个操作数是你要给第一个操作数赋的值。这个OPCode的结果不会用到。(译注:这一点很有意思,赋值语句会返回一个值,这就是我们可以使用$a=$b=1这个语言结构的原因,基本很多语言都是这么做的,但是理论上语句(statement)是不返回值的,例如if语句或者for语句都是不会返回值的,这也是不能把它们赋值给某个变量的原因,在程序设计语言中,能够返回值的都是表达式(expression),有些人吐槽这种设计不合理,因为这违反了一致性原则)
这两个操作数的类型可能不同,这依赖于它们所表示的东西,以及它们是怎么被使用的,我们下面看一下Zend VM所支持的所有操作数类型:
- IS_CV :编译变量(Compiled Variable):这个操作数类型表示一个PHP变量:以$something形式在PHP脚本中出现的变量
- IS_VAR : 供VM内部使用的变量,它可以被其他的OPCode重用,跟$php_variable很像,只是只能供VM内部使用
- IS_TMP_VAR : VM内部使用的变量,但是不能被其他的OPCode重用
- IS_CONST : 表示一个常量,它们都是只读的,它们的值不可改变
- IS_UNUSED :这个表示操作数没有值:这个操作数没有包含任何有意义的东西,可以忽略
ZEND VM的这些类型规范很重要,它们会直接影响整个executor的性能,以及在executor的内存管理中扮演重要的角色。当某个OPCode的handler想读取(fetch/read)保存在某个操作数中的信息时,executor不会执行同样的代码来读取这些信息:而是会针对不同的操作数类型调用不同的(读取)代码。(这个后面 OPCode的专用handler 章节会讲到,简单说就是提前生成所有OPcode的不同操作数之间运算的函数,从而提升性能)
OPCode的专用handler
现在我们知道每个OPCode的handler最多可以接受两个操作数(参数),并且它会根据操作数的类型来获取它们的值。如果每个OPCode的handler代码中都使用switch()语句来选择每个操作数的类型,再根据不同的类型执行不同的读取操作数的值的代码,那么这会导致严重的性能问题,这是由于CPU无法对每个handler中的分支跳转代码进行优化,因为分支跳转在本质上是一种高度动态化的过程。
如果不考虑性能问题,那么ZEND_ADD的handler代码将如下所示(经过简化的伪代码):
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; } /* ... 对op2做同样的事情 .../ /* 对op1_value和op2_value做一些事情 (执行一个算术加法运算?) */ }
现在你要意识到我们是在设计某个OPCode的handler,这个handler可能会在执行PHP脚本的时候被调用很多次。如果每次调用这个handler时都不得不先获取它的操作数的类型,然后根据不同的类型执行不同的读取(fetch/read)代码,这显然不利于程序的性能(不是非常夸张,但仍然存在)。
对于这个问题有一个非常棒的替代方案。
还记得上面提到的PHP源码中对ZEND_ADD这个OPCode的handler的定义么:
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV) { USE_OPLINE zend_free_op free_op1, free_op2; SAVE_OPLINE(); fast_add_function(&EX_T(opline->result.var).tmp_var, GET_OP1_ZVAL_PTR(BP_VAR_R), GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC); FREE_OP1(); FREE_OP2(); CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); }
看看这个奇怪的函数的签名,它甚至都不符合有效C语法(因此它不可能通过C编译器的编译)。
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
这行代码表示ZEND_ADD的handler的第一个操作数op1可以接受CONST、TMP、VAR或者CV类型,op2也是如此。
现在我们再介绍一个神奇的东西:包含这段代码的源文件是zend_vm_def.h,这只是一个模板文件,它会被传给一个处理工具(processor),这个工具会生成每个handler的代码(符合C语言语法的程序),它会对所有操作数类型进行排列组合,然后生成专属于组合中的每一项的handler函数。
我们来算个数,op1可接受5种不同的类型,op2也可以接受5种不同的类型,那个op1和op2的类型组合就有25种情况:上面的工具会为ZEND_ADD生成25个不同的专用于处理特定类型组合的handler函数,这些函数会被写入一个文件中,这个文件会作为PHP源码的一部分被编译。
最终生成的文件名为zend_vm_execute.h,也许你现在正打算点开这个链接,不过你还是建议你三思而后行:因为它真XX的大,别怪我没提醒你噢;-)
现在继续我们算数工作,PHP5.6支持167个OPCode,假设这167个OPCode每个都有可以接受5种操作数类型的op1和op2,那么最终生成的文件会包含4175个C函数。
实际上并非每个OPCode都支持5种不同的操作数类型,所以最终生成的函数个数会小于上面的数字。例如:
ZEND_VM_HANDLER(84, ZEND_FETCH_DIM_W, VAR|CV, CONST|TMP|VAR|UNUSED|CV)
ZEND_FETCH_DIM_W(对某个组合实体(array/object)某个维度的元素进行写操作)的op1只支持两种类型:IS_VAR和IS_CV。
但是zend_vm_execute.h这个文件仍然有大概45000行的C代码,所以正如我之前所建议的,打开这个文件之前请三思,因为打开它可能需要花一点时间。
现在我们小结一下:
- zend_vm_def.h并非有效的C文件,它描述了每个OPCode的handler的特点(使用一种与C接近的自定义语法),每个handler的特点依赖于它的op1和op2的类型,每个操作数最多支持5种类型
- zend_vm_def.h会被传递给一个名为zend_vm_gen.php的PHP脚本,这个脚本位于PHP源码中,它会分析zend_vm_def.h中的特殊语法,会用到很多正则表达式匹配,最终会生成出zend_vm_execute.h这个文件
- zend_vm_def.h在编译PHP源码时不会被处理(这是很显然的)
- zend_vm_execute.h是解析zend_vm_def.h后的输出文件,它包含了符合C语法的代码,它是VM executor的心脏:每个OPCode的专有handler函数都存放在这个文件里,很显然这是一个非常重要的文件
- 当你从源码编译PHP时,PHP源码会提供一个默认的zend_vm_execute.h文件,不过如果你想修改(hack)PHP源码,例如你想添加一个新的OPCode,或者是修改一个已存在的OPCode的行为,你必须先修改(hack)zend_vm_def.h,然后再重新生成zend_vm_execute.h文件
有意思的是:PHP虚拟机的Executor是通过PHP语言自身生成的,哈哈!
我们再来看一个示例:
下面是zend_vm_def.h中定义的ZEND_ADD这个OPCode的handler:
ZEND_VM_HANDLER(1, ZEND_ADD, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
把zend_vm_def.h这个文件传给zend_vm_en.php脚本,将会生成一个新的zend_vm_execute.h文件,这个文件中会包含这个OPCode的专有handler,它们看起来是下面这个样子:
static int ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { /* handler code */ } static int ZEND_FASTCALL ZEND_ADD_SPEC_CONST_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { /* handler code */ } static int ZEND_FASTCALL ZEND_ADD_SPEC_CONST_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { /* handler code */ } static int ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { /* handler code */ } static int ZEND_FASTCALL ZEND_ADD_SPEC_TMP_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { /* handler code */ } static int ZEND_FASTCALL ZEND_ADD_SPEC_TMP_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { /* handler code */ } /* 等等...我们就不列出所有25个函数了! */
所以最终到底执行哪个专有handler是根据op1和op2的类型来确定的,例如:
$a + 2; /* IS_CV + IS_CONST */ /* ZEND_ADD_SPEC_CV_CONST_HANDLER() 这个handler函数会在VM中运行 */
这个函数名是动态生成的,它的生成模式是:ZEND_{OPCODE-NAME}_SPEC_{OP1-TYPE}_{OP2-TYPE}_HANDLER()。
你现在也许会寻思:既然我们必须根据op1和op2的类型来选择专有的handler函数,那么我们岂不是还是要通过一大段switch代码来选择正确的专有handler函数,这跟之前说的使用switch会影响性能有什么差别呢?
我只能告诉你:必须有差别啊,因为操作数的类型可以在编译期解析出,所以编译器会确定在运行期该调用哪个专有handler函数,另外如果你使用了OPCode缓存的话,那编译期的解析工作也会免了。
当PHP编译器在把PHP语言写的程序编译成OPCode时,它知道每个OPCode所接受的op1和op2的类型(因为它是编译器所以它必须知道,这是它的职责)。所以PHP编译器会直接生成一个使用正确的专有handler的OPArray:在执行过程中不会存在其他的选择,不需要使用switch():在运行期直接执行OPCode的专有handler显然会更高效一些。不过如果你修改你的PHP程序,那你必须得重新编译生成一个新的OPArray,这就是OPCode缓存要解决的问题。
性能优化小贴士(Performance tips)
echo一个字符串连接
$foo = 'foo'; $bar = 'bar'; echo $foo . $bar;
下面是这段代码编译后生成的OPArray:
compiled vars: !0 = $foo, !1 = $bar line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > ASSIGN !0, 'foo' 4 1 ASSIGN !1, 'bar' 6 2 CONCAT ~2 !0, !1 3 ECHO ~2 7 4 > RETURN 1
zend引擎会连接(ZEND_CONCAT)$a和$b的值,然后把结果存到一个临时变量(~2)中,这个临时变量的值会被echo出来,然后它会被扔掉。
这几个OPCode意味着什么呢?它们意味着zend引擎即要为一个字符串分配内存空间,还要执行一个复杂的操作:字符串连接——然后再echo这个字符串,最后再把分配的内存释放掉。你可能会觉得一个这么简单的操作尽然要搞得这么复杂,何苦呢?
所以为何不把代码写成下面这个样子:
$foo = 'foo'; $bar = 'bar'; echo $foo , $bar;
compiled vars: !0 = $foo, !1 = $bar line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > ASSIGN !0, 'foo' 4 1 ASSIGN !1, 'bar' 6 2 ECHO !0 3 ECHO !1 7 4 > RETURN 1
看到差别了吧?在echo中使用一个逗号’,’是完全合法的,Zend编译器允许echo语句接受任意多个参数(以逗号分隔),并且它会为每一个参数生成一个ZEND_ECHO的OPCode。这看起来要方便多了。
此时即不用在内存中创建一个临时的缓冲区,也不需要执行字符串连接了。
下面是ZEND_CONCAT这个OPCode的handler的定义
ZEND_VM_HANDLER(8, ZEND_CONCAT, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV) { USE_OPLINE zend_free_op free_op1, free_op2; SAVE_OPLINE(); concat_function(&EX_T(opline->result.var).tmp_var, GET_OP1_ZVAL_PTR(BP_VAR_R), GET_OP2_ZVAL_PTR(BP_VAR_R) TSRMLS_CC); FREE_OP1(); FREE_OP2(); CHECK_EXCEPTION(); ZEND_VM_NEXT_OPCODE(); }
你可以查看concat_function的源码,这个函数会做如下几件事情:
- 检查操作数1是否是字符串,如果不是则把它转换为字符串(重处理,heavy process)(译注:在这里我把heavy process直接翻译为“重处理”,它的意思就是要花一些时间来某个操作)
- 检查操作数2是否是字符串,如果不是则把它转换为字符串(重处理)
- 分配一个缓冲区,确定它的尺寸,然后把连接的结果拷贝到这个缓冲区中,再返回
define()和const
const关键字是从PHP5.3开始引入了,这个关键字跟define()在性能上有很大的差别。
简单概括就是:
- define()是函数调用,所以它必然会承受一些函数调用的开销
- const是一个关键字,它不会被编译成一个会产生函数调用的OPCode,所以它会比define()要更轻量一些
你应该铭记:永远不要使用define()来定义在编译期就知道其值的常量(基本上所有的常量都是如此)。
define('FOO', 'foo'); echo FOO;
line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > SEND_VAL 'FOO' 1 SEND_VAL 'foo' 2 DO_FCALL 2 'define' 5 3 FETCH_CONSTANT ~1 'FOO' 4 ECHO ~1 6 5 > RETURN 1
define()会导致函数调用,它会把常量注册到引擎中,这样之后ZEND_FETCH_CONSTANT这个OPCode就可以直接读取常量的值。
下面再看看const的情况:
const FOO = 'foo'; echo FOO; line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > DECLARE_CONST 'FOO', 'foo' 5 1 FETCH_CONSTANT ~0 'FOO' 2 ECHO ~0 6 3 > RETURN 1
所有跟函数调用相关(用于define())的OPCode都消失了,它们被一个更轻量级的OPCode(DECLARE_CONST)所取代。
不过对于const和define()而言,还有一些小问题,不过这都是逻辑上的问题:
- const不能声明条件常量
- const(DECLARE_CONST)不能使用任何除IS_CONST外的其他操作数类型
这意味着你不能在下面的代码中使用const,但是可以使用define():
if (foo()) { const FOO = 'foo'; /* 编译器会禁止这种写法 */ }
下面的写法也不行:
$a = 'FOO'; const $a = 'foo';
尽管const结构在性能上表现更好,但它也会影响你的代码的动态性。
动态函数调用
先看一个最简单的函数调用:
function foo() { } foo();
line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > NOP 5 1 DO_FCALL 0 'foo' 6 2 > RETURN 1
NOP表示“没有操作(No Operation)”。编译器之所以会生成这个OPCode因为一些历史原因:-)。NOP的执行时间是真正的0秒,你不用管它们(OPCache这种优化器会把它们都剔除掉)。
上面的代码只生成了一个DO_FCALL的OPCode,这个OPCode就是调用函数foo()的OPCode。好了,这个示例没什么可说的了。
我们再看一个动态函数调用:
function foo() { } $a = 'foo'; $a();
line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > NOP 6 1 ASSIGN !0, 'foo' 7 2 INIT_FCALL_BY_NAME !0 3 DO_FCALL_BY_NAME 0 9 4 > RETURN 1
这个函数调用使用了两个OPCode,而不像之前一样只有一个,似乎开始有点不利于整体性能的感觉了(不过我们还是先看下这些OPCode的handler再下结论)。首先你要知道多出的这个OPCode(INIT_FCALL_BY_NAME)之所以会在此出现,是因为编译器在编译的时候并不知道你想调用哪个函数,因为此时函数名被保存在一个变量中了(动态函数调用)。
记住一点编译器不能解释(interpret)变量,编译器在编译时并不知道变量中保存的是什么。PHP程序中的变量会被编译为CV,根据定义它们是动态的,它们可以保存任何信息(它们可能是NULL值,或者甚至是“undefined variables”,在编译期间谁能知道呢?)。所以在这种情况下,编译器没有其他选择,除了把函数调用的准备工作和查询符号表的工作推迟到运行期进行,这不利于性能,这是因为有些本可以在编译器完成的工作,现在必须得推迟到运行期进行。
从更广泛地的角度来说,你需要记住一条常识:你使用越多PHP语言的动态特性,executor就需要做越多的工作来执行你的PHP代码,你的代码的整体性能也会越差。
这种情况对方法(method,类的方法)也同样适用,除了一点小差别外,这点差别对性能可能会有较大的影响:类在运行时可能不存在,这可能会触发自动加载(autoload
),这会带来巨大的性能差异。不过使用OPCode的缓存可以显著降低这些不利的影响。
类的延迟绑定(Delayed class binding)
现在要讨论的这个话题是蛋糕上的奶油:类和继承。
再强调一句:为了性能考虑,当在声明class A继承class B之前,你最好已经定义了B,如果没有:只会再一次增加运行时的负担。
我们来看看代码:
class Bar { } class Foo extends Bar { }
compiled vars: none line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > NOP 5 1 NOP 2 NOP 6 3 > RETURN 1
上面的代码没任何问题:如果你以正确的方式写代码,以正确的顺序定义类,那么编译器可以接管所有跟类声明相关的繁琐工作。你可以从上面输出的OPCode中看出executor做了什么事情没?NOP、NOP,再NOP:什么都没做(OPCache优化器甚至会删除所有的这些开销超级小的NOP)。
编译器接管了这个工作(声明一个类是一项耗费性能工作),再次强调下如果你使用OPCode缓存,那么你甚至可以根本不用再管编译期的事情
所以根据上面的执行情况来看,在PHP中声明类实际上是非常轻量级的工作,当然前提是你把类声明的顺序搞对
class Foo extends Bar { } class Bar { }
compiled vars: none line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > FETCH_CLASS 4 :0 'Bar' 1 DECLARE_INHERITED_CLASS '%00foo%2Ftmp%2Ffoo.php0x7f198b0c401d', 'foo' 5 2 NOP 6 3 > RETURN 1
上面的代码中我们同样声明了一个类Foo,它继承自Bar,但是在编译器读取这个声明的时候,它并不知道任何关于Bar的信息。在这种情况下,为了在executor中执行Foo的代码,编译器要怎么给Foo准备内存呢?编译器什么都做不了:如果这种情况出现在动态特性不强的语言中,上面的代码会产生一个编译错误:“解析错误:无法找到类(Parse error: class not found)”,根本不会进入执行期。显然PHP的动态特性很强,所以它的编译器不会报错。
此时编译器只能再次把类的声明推迟到运行期进行(PHP允许这么做),这对于引擎来说是一件很繁琐的工作,它的繁琐之处在于要解析整个继承树(inheritence tree),并且要把所有的父类的方法添加到声明的类中,这些工作本来是可以在编译期完成的,但是在上面这个示例中是不可行的:这会给运行期增加负担,并且只要这个类的声明方式不变,这种负担会不断叠加。从性能角度来说,这种写法真是2得不行了啊!
再说一次,我们需要忍受PHP动态特性的折磨,因为它允许使用一个未经过编译的类的对象(使用自动加载(autoloaded)?)。确实这些特性让PHP看起来很灵活,很好用,但是你的机器也会为你的懒惰付出代价。不过如果你使用OPCode缓存,特别是OPCache,它们会非常有效地优化这种情况。
你还没搞清楚?那我们再看一个例子:
class Foo { }
compiled vars: none line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > NOP 4 1 > RETURN 1
上面的代码在运行的时候没有任何特殊的OPCode,就跟之前的示例一样。我们在上面的代码中加一点动态特性:
if ($a) { class Foo { } }
compiled vars: !0 = $a line #* I O op fetch ext return operands ----------------------------------------------------------------------------------- 3 0 > > JMPZ !0, ->3 4 1 > DECLARE_CLASS $0 '%00foo%2Ftmp%2Ffoo.php0x7fcef3f9701d', 'foo' 5 2 > JMP ->3 6 3 > > RETURN 1
你可以看到动态特性的代价:现在这个类完全是在运行期声明和解析的(ZEND_DECLARE_CLASS这个OPCode就是干这个事的),你每次运行这段代码都会进行这些处理。现在搞清楚了吧!
总结
这篇文章展示了Zend虚拟机中的核心部分:executor的代码。它是PHP源码中完成“真正”的执行PHP程序的部分:它会执行PHP脚本被转换成的每个单个任务(每个OPCode)。它是PHP源码中对PHP脚本的执行性能有最关键影响的部分,所以在设计它的时候,性能被作为第一因素进行考虑。
这也是为什么在你对软件虚拟机的设计或者是对底层程序设计并不熟悉时阅读这些代码时会困惑于这些代码为何要写成这个样子的原因,它们对你而言似乎非常复杂。对于这个问题的唯一答案就是性能。C语言是唯一可以实现这些细节层面优化的语言,至少在我所知道的语言中是如此,C代码会被直接编译为目标机器的汇编指令,现如今它的编译器已经非常成熟,大多数C编译器都已经差不多有40多年的历史了。
你要知道事实上PHP虚拟机,包括它的整个源码都已经经过了差不多20年的修改、重构和优化了,所以请相信我,如果PHP要你以某种方式才能更高效地完成某件事情,那么请接受它,这些都是经过精心设计的,而不是随机为之的。为了优化某些底层部分的性能,我们甚至会阅读不同编译器编译executor所生成的汇编代码,从中寻找哪些地方可以进一步优化,从而让编译器生成出更优化的代码(有很多技巧可以优化C编译器生成的代码)。另外Zend虚拟机中的一些关键部分甚至是直接用汇编语言写成的(不多,但还是有一些)。
最后插播一个小广告,我目前的主要工作是开发Blackfile性能分析器这个扩展,它可以告诉你的代码中有哪些性能问题,例如上面我所介绍的哪些会引起性能问题的代码,当然还有其他一些功能在此我就不详细介绍了,有兴趣的同学可以自己去看看;-)
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/140315.html