使用ZEND_VM_REPEATABLE_OPCODE减少Zend虚拟机函数调用

本文基于PHP8.0.1

我们先来看看对应的宏:

1
2
3
4
5
6
#define ZEND_VM_REPEATABLE_OPCODE \
do {
#define ZEND_VM_REPEAT_OPCODE(_opcode) \
} while (UNEXPECTED((++opline)->opcode == _opcode)); \
OPLINE = opline; \
ZEND_VM_CONTINUE()

可以看到,在ZEND_VM_REPEATABLE_OPCODEZEND_VM_REPEAT_OPCODE两个宏之间,会判断下一个opcode是否和当前的opcode一样,如果一样,那么再次进入循环。这可以运用在oplinehandler里面,比如说如下脚本:

1
2
3
4
5
6
7
8
<?php

function foo($a = 1, $b = 2)
{
var_dump($a, $b);
}

foo();

函数foo生成的opcodes如下:

1
2
3
4
5
6
7
8
L3-6 foo() /Users/codinghuang/.phpbrew/build/php-8.0.1/test5.php - 0x10e85f3c0 + 7 ops
L3 #0 RECV_INIT 1 1 $a
L3 #1 RECV_INIT 2 2 $b
L5 #2 INIT_FCALL<2> 112 "var_dump"
L5 #3 SEND_VAR $a 1
L5 #4 SEND_VAR $b 2
L5 #5 DO_ICALL
L6 #6 RETURN<-1> null

可以看到,当函数有默认参数的时候,会通过这个ZEND_RECV_INIT来接收参数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_RECV_INIT_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
uint32_t arg_num;
zval *param;

ZEND_VM_REPEATABLE_OPCODE

arg_num = opline->op1.num;
param = EX_VAR(opline->result.var);
if (arg_num > EX_NUM_ARGS()) {
zval *default_value = RT_CONSTANT(opline, opline->op2);

if (Z_OPT_TYPE_P(default_value) == IS_CONSTANT_AST) {
zval *cache_val = (zval*)CACHE_ADDR(Z_CACHE_SLOT_P(default_value));

/* we keep in cache only not refcounted values */
if (Z_TYPE_P(cache_val) != IS_UNDEF) {
ZVAL_COPY_VALUE(param, cache_val);
} else {
SAVE_OPLINE();
ZVAL_COPY(param, default_value);
if (UNEXPECTED(zval_update_constant_ex(param, EX(func)->op_array.scope) != SUCCESS)) {
zval_ptr_dtor_nogc(param);
ZVAL_UNDEF(param);
HANDLE_EXCEPTION();
}
if (!Z_REFCOUNTED_P(param)) {
ZVAL_COPY_VALUE(cache_val, param);
}
}
goto recv_init_check_type;
} else {
ZVAL_COPY(param, default_value);
}
} else {
recv_init_check_type:
if (UNEXPECTED((EX(func)->op_array.fn_flags & ZEND_ACC_HAS_TYPE_HINTS) != 0)) {
SAVE_OPLINE();
if (UNEXPECTED(!zend_verify_recv_arg_type(EX(func), arg_num, param, CACHE_ADDR(opline->extended_value)))) {
HANDLE_EXCEPTION();
}
}
}

ZEND_VM_REPEAT_OPCODE(ZEND_RECV_INIT);
ZEND_VM_NEXT_OPCODE();
}

这里就用到了这个优化,连续的两个opcode是一样的,所以在第一条ZEND_RECV_INIT执行完之后,不会退出这个函数,而是回到了ZEND_VM_REPEATABLE_OPCODE处,继续执行下一条opline