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)