Skip to content

Commit 1bbd75c

Browse files
committed
Zend: elide redundant type check on typed property writes
1 parent 92128ac commit 1bbd75c

10 files changed

Lines changed: 207 additions & 59 deletions

Zend/Optimizer/dfa_pass.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,48 @@ void zend_dfa_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *ctx
10871087
#endif
10881088
}
10891089

1090+
/* Elide the run-time type check on typed property writes (ASSIGN_OBJ)
1091+
* when the assigned value is statically proven to already satisfy the
1092+
* property type, so no verification or coercion is required. The flag is
1093+
* read by the ASSIGN_OBJ handler from the following OP_DATA opline. */
1094+
for (int i = 0; i < (int) op_array->last; i++) {
1095+
zend_op *op = op_array->opcodes + i;
1096+
if (op->opcode != ZEND_ASSIGN_OBJ) {
1097+
continue;
1098+
}
1099+
1100+
const zend_property_info *prop_info = zend_fetch_prop_info(op_array, ssa, op, &ssa->ops[i]);
1101+
if (!prop_info
1102+
|| !ZEND_TYPE_IS_SET(prop_info->type)
1103+
|| !ZEND_TYPE_IS_ONLY_MASK(prop_info->type)
1104+
|| prop_info->hooks
1105+
|| (prop_info->flags & (ZEND_ACC_READONLY | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_VIRTUAL))) {
1106+
continue;
1107+
}
1108+
1109+
/* The assigned value lives in the following OP_DATA opline. */
1110+
const zend_op *data = op + 1;
1111+
uint32_t val_type;
1112+
if (data->op1_type == IS_CONST) {
1113+
val_type = _const_op_type(CRT_CONSTANT(data->op1));
1114+
} else if (ssa->ops[i + 1].op1_use >= 0) {
1115+
val_type = ssa->var_info[ssa->ops[i + 1].op1_use].type;
1116+
} else {
1117+
continue;
1118+
}
1119+
1120+
if (val_type & (MAY_BE_REF | MAY_BE_UNDEF)) {
1121+
continue;
1122+
}
1123+
1124+
uint32_t pure = val_type & MAY_BE_ANY;
1125+
if (!pure || (pure & ~ZEND_TYPE_PURE_MASK(prop_info->type))) {
1126+
continue;
1127+
}
1128+
1129+
op_array->opcodes[i + 1].extended_value |= ZEND_ASSIGN_OBJ_SKIP_TYPE_CHECK;
1130+
}
1131+
10901132
for (v = op_array->last_var; v < ssa->vars_count; v++) {
10911133

10921134
op_1 = ssa->vars[v].definition;

Zend/Optimizer/zend_dump.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,10 @@ ZEND_API void zend_dump_op(const zend_op_array *op_array, const zend_basic_block
770770
}
771771
}
772772
}
773+
/* ASSIGN_OBJ carries the elision flag on its following OP_DATA opline. */
774+
if (opline->opcode == ZEND_ASSIGN_OBJ && ((opline + 1)->extended_value & ZEND_ASSIGN_OBJ_SKIP_TYPE_CHECK)) {
775+
fprintf(stderr, " (skip type check)");
776+
}
773777
}
774778

775779
ZEND_API void zend_dump_op_line(const zend_op_array *op_array, const zend_basic_block *b, const zend_op *opline, uint32_t dump_flags, const void *data)

Zend/Optimizer/zend_inference.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2432,7 +2432,7 @@ static const zend_property_info *lookup_prop_info(const zend_class_entry *ce, ze
24322432
return NULL;
24332433
}
24342434

2435-
static const zend_property_info *zend_fetch_prop_info(const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline, const zend_ssa_op *ssa_op)
2435+
ZEND_API const zend_property_info *zend_fetch_prop_info(const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline, const zend_ssa_op *ssa_op)
24362436
{
24372437
const zend_property_info *prop_info = NULL;
24382438
if (opline->op2_type == IS_CONST) {

Zend/Optimizer/zend_inference.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ ZEND_API uint32_t zend_array_element_type(uint32_t t1, uint8_t op_type, bool wri
223223

224224
ZEND_API bool zend_inference_propagate_range(const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline, const zend_ssa_op* ssa_op, int var, zend_ssa_range *tmp);
225225

226+
ZEND_API const zend_property_info *zend_fetch_prop_info(const zend_op_array *op_array, const zend_ssa *ssa, const zend_op *opline, const zend_ssa_op *ssa_op);
227+
226228
ZEND_API uint32_t zend_fetch_arg_info_type(
227229
const zend_script *script, const zend_arg_info *arg_info, zend_class_entry **pce);
228230
ZEND_API void zend_init_func_return_info(

Zend/zend_compile.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,11 @@ static zend_always_inline bool zend_check_arg_send_type(const zend_function *zf,
12061206
#define ZEND_RETURNS_FUNCTION (1<<0)
12071207
#define ZEND_RETURNS_VALUE (1<<1)
12081208

1209+
/* Stored in the OP_DATA opline's extended_value for ZEND_ASSIGN_OBJ. Set by the
1210+
* optimizer when the assigned value is statically proven to already satisfy the
1211+
* (typed) property, so the run-time type verification can be skipped. */
1212+
#define ZEND_ASSIGN_OBJ_SKIP_TYPE_CHECK (1<<0)
1213+
12091214
#define ZEND_ARRAY_ELEMENT_REF (1<<0)
12101215
#define ZEND_ARRAY_NOT_PACKED (1<<1)
12111216
#define ZEND_ARRAY_SIZE_SHIFT 2

Zend/zend_vm_def.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2523,7 +2523,7 @@ ZEND_VM_C_LABEL(assign_object):
25232523
ZEND_VM_C_LABEL(assign_obj_simple):
25242524
property_val = OBJ_PROP(zobj, prop_offset);
25252525
if (Z_TYPE_P(property_val) != IS_UNDEF) {
2526-
if (prop_info != NULL) {
2526+
if (prop_info != NULL && !((opline+1)->extended_value & ZEND_ASSIGN_OBJ_SKIP_TYPE_CHECK)) {
25272527
value = zend_assign_to_typed_prop(prop_info, property_val, value, &garbage EXECUTE_DATA_CC);
25282528
ZEND_VM_C_GOTO(free_and_exit_assign_obj);
25292529
} else {

Zend/zend_vm_execute.h

Lines changed: 58 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/opcache/tests/named_parameter_new.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ MyClass::__construct:
4949
0001 CV1($bar) = RECV_INIT 2 int(0)
5050
0002 ASSIGN_OBJ THIS string("foo")
5151
0003 OP_DATA CV0($foo)
52-
0004 ASSIGN_OBJ THIS string("bar")
52+
0004 ASSIGN_OBJ THIS string("bar") (skip type check)
5353
0005 OP_DATA CV1($bar)
5454
0006 RETURN null
5555

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Elide the type check on a typed property write when the value provably satisfies the property type
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.opt_debug_level=0x20000
8+
--EXTENSIONS--
9+
opcache
10+
--FILE--
11+
<?php
12+
class C {
13+
public int $i = 0;
14+
public float $f = 0.0;
15+
public function exact(int $i): void { $this->i = $i; }
16+
public function coerce(int $x): void { $this->f = $x; }
17+
}
18+
echo "done\n";
19+
?>
20+
--EXPECTF--
21+
$_main:
22+
; (lines=2, args=0, vars=0, tmps=0)
23+
; (after optimizer)
24+
; %s:1-10
25+
0000 ECHO string("done\n")
26+
0001 RETURN int(1)
27+
28+
C::exact:
29+
; (lines=4, args=1, vars=1, tmps=0)
30+
; (after optimizer)
31+
; %s:5-5
32+
0000 CV0($i) = RECV 1
33+
0001 ASSIGN_OBJ THIS string("i") (skip type check)
34+
0002 OP_DATA CV0($i)
35+
0003 RETURN null
36+
37+
C::coerce:
38+
; (lines=4, args=1, vars=1, tmps=0)
39+
; (after optimizer)
40+
; %s:6-6
41+
0000 CV0($x) = RECV 1
42+
0001 ASSIGN_OBJ THIS string("f")
43+
0002 OP_DATA CV0($x)
44+
0003 RETURN null
45+
done
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
--TEST--
2+
Typed property write check elision preserves runtime behavior (exact match, coercion, both modes)
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
--EXTENSIONS--
8+
opcache
9+
--FILE--
10+
<?php
11+
class C {
12+
public int $i = 0;
13+
public float $f = 0.0;
14+
public string $s = '';
15+
public bool $b = false;
16+
public ?int $ni = 0;
17+
public function __construct(int $i, string $s) { $this->i = $i; $this->s = $s; }
18+
public function setBool(bool $b): void { $this->b = $b; }
19+
public function setNullable(?int $ni): void { $this->ni = $ni; }
20+
public function coerceToFloat(int $x): void { $this->f = $x; }
21+
}
22+
23+
$c = new C(5, "hello");
24+
var_dump($c->i, $c->s);
25+
$c->setBool(true);
26+
var_dump($c->b);
27+
$c->setNullable(null);
28+
var_dump($c->ni);
29+
$c->setNullable(7);
30+
var_dump($c->ni);
31+
$c->coerceToFloat(3);
32+
var_dump($c->f);
33+
34+
class Promoted {
35+
public function __construct(public int $x, public readonly string $y) {}
36+
}
37+
$p = new Promoted(1, "ok");
38+
var_dump($p->x, $p->y);
39+
?>
40+
--EXPECT--
41+
int(5)
42+
string(5) "hello"
43+
bool(true)
44+
NULL
45+
int(7)
46+
float(3)
47+
int(1)
48+
string(2) "ok"

0 commit comments

Comments
 (0)