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代码。
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关键字。
简单地说:
conast是关键字,不会产生函数调用,要比define()轻量许多
define('FOO', 'foo'); echo FOO;
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在使用上有一些限制:
动态函数调用
尽量不要使用动态的函数名去调用函数:
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要轻量许多。