Commit fabcc91a524 for php
commit fabcc91a524bdcd07563547b33d59dd5cc0c6372
Author: Nora Dossche <7771979+ndossche@users.noreply.github.com>
Date: Thu Oct 15 18:52:02 2026 +0200
JIT: Fold IS_UNDEF check into dereference of typed property reads for tracing JIT (#23939)
When the tracing JIT reads a typed property with a known result type into
a reg, the value is either of that type, a reference or IS_UNDEF.
Instead of guarding IS_UNDEF separately before making a decision on the
type, exit only if the value is neither of the expected type nor a reference.
This removes a compare and branch from the hot path of, e.g., reads of
$this->prop in loops.
Example PHP code:
```php
class Counter {
public int $x = 1;
function sum($n) {
$s = 0;
for ($i = 0; $i < $n; $i++) {
$s += $this->x;
}
return $s;
}
}
var_dump((new Counter)->sum(1000));
```
The loop compiled previously to:
```asm
.L1:
movq (%rdi), %rsi
leaq 8(%rsi), %rdi
.L2:
movq (%rdi), %rsi
movq %rsi, %rdi
addq %rcx, %rdi
jo jit$$trace_exit_5
leaq 1(%rdx), %rdx
movabsq $EG(vm_interrupt), %rcx
movb (%rcx), %cl
testb %cl, %cl
jne jit$$trace_exit_6
movq %rdi, %rcx
.L3:
cmpq %rax, %rdx
jge jit$$trace_exit_3
movq 0x20(%r14), %rsi
leaq 0x28(%rsi), %rdi
movl 0x30(%rsi), %esi
testl %esi, %esi
je jit$$trace_exit_4
cmpl $4, %esi
je .L2
jmp .L1
```
Notice that in `.L3` there's two checks on `%esi` (the type info): one
for the IS_UNDEF (via `testl`) and one to check for an int (`$4`).
The new code moves the check for reference type to the `.L1` header and
only checks for the int type in `.L3`, reducing the code executed in the
hot loop:
```asm
.L1:
cmpl $0x30a, %esi
jne jit$$trace_exit_4
movq (%rdi), %rsi
leaq 8(%rsi), %rdi
.L2:
movq (%rdi), %rsi
movq %rsi, %rdi
addq %rcx, %rdi
jo jit$$trace_exit_5
leaq 1(%rdx), %rdx
movabsq $EG(vm_interrupt), %rcx
movb (%rcx), %cl
testb %cl, %cl
jne jit$$trace_exit_6
movq %rdi, %rcx
.L3:
cmpq %rax, %rdx
jge jit$$trace_exit_3
movq 0x20(%r14), %rsi
leaq 0x28(%rsi), %rdi
movl 0x30(%rsi), %esi
cmpl $4, %esi
je .L2
jmp .L1
```
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index c1826138174..188144de495 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -11896,29 +11896,27 @@ static int zend_jit_rope(zend_jit_ctx *jit, const zend_op *opline, uint32_t op2_
return 1;
}
-static int zend_jit_zval_copy_deref_reg(zend_jit_ctx *jit, zend_jit_addr res_addr, uint32_t res_info, zend_jit_addr val_addr, ir_ref type, ir_ref *values)
+/* If exit_addr is set, the value may also be IS_UNDEF */
+static int zend_jit_zval_copy_deref_reg(zend_jit_ctx *jit, zend_jit_addr res_addr, uint32_t res_info, zend_jit_addr val_addr, ir_ref type, ir_ref *values, const void *exit_addr)
{
ir_ref if_type, val;
if (res_info == MAY_BE_LONG) {
if_type = ir_IF(ir_EQ(type, ir_CONST_U32(IS_LONG)));
- ir_IF_TRUE(if_type);
- val = jit_ZVAL_ADDR(jit, val_addr);
- ir_END_PHI_list(*values, val);
- ir_IF_FALSE(if_type);
- val = ir_ADD_OFFSET(jit_Z_PTR(jit, val_addr), offsetof(zend_reference, val));
- ir_END_PHI_list(*values, val);
} else if (res_info == MAY_BE_DOUBLE) {
if_type = ir_IF(ir_EQ(type, ir_CONST_U32(IS_DOUBLE)));
- ir_IF_TRUE(if_type);
- val = jit_ZVAL_ADDR(jit, val_addr);
- ir_END_PHI_list(*values, val);
- ir_IF_FALSE(if_type);
- val = ir_ADD_OFFSET(jit_Z_PTR(jit, val_addr), offsetof(zend_reference, val));
- ir_END_PHI_list(*values, val);
} else {
ZEND_UNREACHABLE();
}
+ ir_IF_TRUE(if_type);
+ val = jit_ZVAL_ADDR(jit, val_addr);
+ ir_END_PHI_list(*values, val);
+ ir_IF_FALSE(if_type);
+ if (exit_addr) {
+ ir_GUARD(ir_EQ(type, ir_CONST_U32(IS_REFERENCE_EX)), ir_CONST_ADDR(exit_addr));
+ }
+ val = ir_ADD_OFFSET(jit_Z_PTR(jit, val_addr), offsetof(zend_reference, val));
+ ir_END_PHI_list(*values, val);
return 1;
}
@@ -14392,6 +14390,7 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit,
ir_ref end_inputs = IR_UNUSED;
ir_ref slow_inputs = IR_UNUSED;
ir_ref end_values = IR_UNUSED;
+ const void *undef_exit_addr = NULL;
ZEND_ASSERT(opline->op2_type == IS_CONST);
ZEND_ASSERT(op1_info & MAY_BE_OBJECT);
@@ -14646,8 +14645,15 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit,
if (!exit_addr) {
return 0;
}
- prop_type_ref = jit_Z_TYPE_INFO(jit, prop_addr);
- ir_GUARD(prop_type_ref, ir_CONST_ADDR(exit_addr));
+ if (opline->opcode != ZEND_FETCH_OBJ_W
+ && !(res_info & MAY_BE_GUARD)
+ && Z_MODE(res_addr) == IS_REG) {
+ /* perform IS_UNDEF check together with the dereference of the result */
+ undef_exit_addr = exit_addr;
+ } else {
+ prop_type_ref = jit_Z_TYPE_INFO(jit, prop_addr);
+ ir_GUARD(prop_type_ref, ir_CONST_ADDR(exit_addr));
+ }
}
} else {
prop_type_ref = jit_Z_TYPE_INFO(jit, prop_addr);
@@ -14761,7 +14767,7 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit,
} else if (Z_MODE(res_addr) == IS_REG) {
prop_type_ref = jit_Z_TYPE_INFO(jit, prop_addr);
- if (!zend_jit_zval_copy_deref_reg(jit, res_addr, res_info & ~MAY_BE_GUARD, prop_addr, prop_type_ref, &end_values)) {
+ if (!zend_jit_zval_copy_deref_reg(jit, res_addr, res_info & ~MAY_BE_GUARD, prop_addr, prop_type_ref, &end_values, undef_exit_addr)) {
return 0;
}
} else {
diff --git a/ext/opcache/tests/jit/fetch_obj_011.phpt b/ext/opcache/tests/jit/fetch_obj_011.phpt
new file mode 100644
index 00000000000..0f5f8c10fd6
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_obj_011.phpt
@@ -0,0 +1,82 @@
+--TEST--
+JIT FETCH_OBJ: Typed property that becomes a reference or undefined
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_update_protection=0
+opcache.jit=tracing
+opcache.jit_hot_loop=1
+opcache.jit_hot_func=1
+opcache.jit_hot_return=1
+opcache.jit_hot_side_exit=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+
+class A {
+ public int $x = 1;
+ public float $f = 1.5;
+
+ function becomes_ref() {
+ $s = 0;
+ for ($i = 0; $i < 300; $i++) {
+ $s += $this->x;
+ if ($i == 150) {
+ $r = &$this->x;
+ $r = 2;
+ }
+ }
+ return $s;
+ }
+
+ function becomes_ref_float() {
+ $s = 0.0;
+ for ($i = 0; $i < 300; $i++) {
+ $s += $this->f;
+ if ($i == 150) {
+ $r = &$this->f;
+ $r = 2.5;
+ }
+ }
+ return $s;
+ }
+
+ function unset_isset() {
+ $s = 0;
+ for ($i = 0; $i < 300; $i++) {
+ $s += $this->x ?? 7;
+ if ($i == 150) unset($this->x);
+ }
+ return $s;
+ }
+}
+
+class M {
+ public int $x = 1;
+
+ function __get($name) {
+ return 1000;
+ }
+
+ function unset_get() {
+ $s = 0;
+ for ($i = 0; $i < 300; $i++) {
+ $s += $this->x;
+ if ($i == 150) unset($this->x);
+ }
+ return $s;
+ }
+}
+
+var_dump((new A)->becomes_ref());
+var_dump((new A)->becomes_ref_float());
+var_dump((new A)->unset_isset());
+var_dump((new M)->unset_get());
+
+?>
+--EXPECT--
+int(449)
+float(599)
+int(1194)
+int(149151)