Commit 5b589114723 for php
commit 5b589114723057c37accb154a26d857b3e0a3fc7
Author: Nora Dossche <7771979+ndossche@users.noreply.github.com>
Date: Tue Nov 10 11:17:12 2026 +0100
Implement fast-path static property access in JIT (#25042)
* Factor out jit_called_scope()
This includes the fix from GH-25041.
* Implement fast-path static:: property access in JIT
This is common for OOP applications, in particularly noticed in the
Laravel framework.
* Test
* Adapt test
diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c
index d5d61c3289b..1b95e9e2e26 100644
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ -2535,7 +2535,8 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
&& (opline->op2_type == IS_CONST
|| (opline->op2_type == IS_UNUSED
&& ((opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF
- || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT))))) {
+ || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT
+ || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC))))) {
break;
}
if (!zend_jit_fetch_static_prop(&ctx, opline, op_array)) {
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 7e110e6361f..0f50d6915b3 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -9322,6 +9322,31 @@ static int zend_jit_init_method_call(zend_jit_ctx *jit,
return 1;
}
+/* Called scope of the current frame, as used by static:: and inherited by self::/parent:: calls. */
+static ir_ref jit_called_scope(zend_jit_ctx *jit, const zend_op_array *op_array)
+{
+ if (op_array->fn_flags & ZEND_ACC_STATIC) {
+ // JIT: Z_CE(EX(This))
+ return ir_LOAD_A(jit_EX(This.value.ref));
+ } else if (op_array->scope
+ && op_array->function_name
+ && !(op_array->fn_flags & ZEND_ACC_CLOSURE)) {
+ // JIT: Z_OBJCE(EX(This))
+ return ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce)));
+ } else {
+ /* Closures can be unbound, file/eval inherits This from the existing frame's scope. */
+ ir_ref if_object, values = IR_UNUSED;
+
+ if_object = ir_IF(ir_EQ(jit_Z_TYPE_ref(jit, jit_EX(This)), ir_CONST_U8(IS_OBJECT)));
+ ir_IF_TRUE(if_object);
+ ir_END_PHI_list(values,
+ ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce))));
+ ir_IF_FALSE(if_object);
+ ir_END_PHI_list(values, ir_LOAD_A(jit_EX(This.value.ref)));
+ return ir_PHI_list(values);
+ }
+}
+
static int zend_jit_init_static_method_call(zend_jit_ctx *jit,
const zend_op *opline,
uint32_t b,
@@ -9453,22 +9478,7 @@ static int zend_jit_init_static_method_call(zend_jit_ctx *jit,
if (opline->op1_type == IS_UNUSED
&& ((opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT ||
(opline->op1.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF)) {
- if (op_array->fn_flags & ZEND_ACC_STATIC) {
- scope_ref = ir_LOAD_A(jit_EX(This.value.ref));
- } else if ((op_array->fn_flags & ZEND_ACC_CLOSURE) || !op_array->function_name) {
- /* Closures can be unbound, file/eval inherits This from the existing frame's scope. */
- ir_ref if_object, values = IR_UNUSED;
-
- if_object = ir_IF(ir_EQ(jit_Z_TYPE_ref(jit, jit_EX(This)), ir_CONST_U8(IS_OBJECT)));
- ir_IF_TRUE(if_object);
- ir_END_PHI_list(values,
- ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce))));
- ir_IF_FALSE(if_object);
- ir_END_PHI_list(values, ir_LOAD_A(jit_EX(This.value.ref)));
- scope_ref = ir_PHI_list(values);
- } else {
- scope_ref = ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(This.value.ref)), offsetof(zend_object, ce)));
- }
+ scope_ref = jit_called_scope(jit, op_array);
}
if (!zend_jit_push_call_frame(jit, opline, op_array, func, false, false, checked_stack, func_ref, scope_ref)) {
return 0;
@@ -16377,11 +16387,14 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline,
uint32_t cache_slot = opline->extended_value & ~ZEND_FETCH_OBJ_FLAGS;
uint32_t flags;
ir_ref ref, ref2, if_cached, fast_path, cold_path, prop_info_ref, if_typed, if_def;
+ ir_ref slow_inputs = IR_UNUSED;
int fetch_type;
zend_property_info *known_prop_info = NULL;
zend_class_entry *ce;
+ bool is_static = opline->op2_type == IS_UNUSED
+ && (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC;
- ce = zend_get_known_class(op_array, opline, opline->op2_type, opline->op2);
+ ce = is_static ? NULL : zend_get_known_class(op_array, opline, opline->op2_type, opline->op2);
if (ce) {
zval *zv = RT_CONSTANT(opline, opline->op1);
zend_string *prop_name;
@@ -16424,6 +16437,17 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline,
default: ZEND_UNREACHABLE();
}
+ if (is_static) {
+ /* The cache slot is polymorphic on the called scope. */
+ // JIT: if (CACHED_PTR(cache_slot) == called_scope)
+ ir_ref if_scope = ir_IF(ir_EQ(
+ ir_LOAD_A(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot)),
+ jit_called_scope(jit, op_array)));
+ ir_IF_FALSE_cold(if_scope);
+ ir_END_list(slow_inputs);
+ ir_IF_TRUE(if_scope);
+ }
+
// JIT: result = CACHED_PTR(cache_slot + sizeof(void *));
ref = ir_LOAD_A(
ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot + sizeof(void*)));
@@ -16499,6 +16523,10 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline,
fast_path = ir_END();
ir_IF_FALSE_cold(if_cached);
+ if (slow_inputs) {
+ ir_END_list(slow_inputs);
+ ir_MERGE_list(slow_inputs);
+ }
jit_SET_EX_OPLINE(jit, opline);
ref2 = ir_CALL_2(IR_ADDR, ir_CONST_FC_FUNC(zend_fetch_static_property), jit_FP(jit), ir_CONST_I32(fetch_type));
zend_jit_check_exception_undef_result(jit, opline);
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index e8705794281..01d1ff50811 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -6212,7 +6212,8 @@ static zend_vm_opcode_handler_t zend_jit_trace(zend_jit_trace_rec *trace_buffer,
&& (opline->op2_type == IS_CONST
|| (opline->op2_type == IS_UNUSED
&& ((opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_SELF
- || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT))))) {
+ || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_PARENT
+ || (opline->op2.num & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_STATIC))))) {
break;
}
if (!zend_jit_fetch_static_prop(&ctx, opline, op_array)) {
diff --git a/ext/opcache/tests/jit/fetch_static_prop_002.phpt b/ext/opcache/tests/jit/fetch_static_prop_002.phpt
new file mode 100644
index 00000000000..07a2957765a
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_static_prop_002.phpt
@@ -0,0 +1,112 @@
+--TEST--
+FETCH_STATIC_PROP_* with static:: must respect the called scope
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+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
+
+trait T {
+ public static $t = 'trait';
+
+ public function getT() {
+ return static::$t;
+ }
+}
+
+class A {
+ use T;
+ public static $v = 'A';
+ public static array $arr = [];
+ public static int $typed;
+
+ public static function getStatic() {
+ return static::$v;
+ }
+
+ public function getInstance() {
+ return static::$v;
+ }
+
+ public function getClosure() {
+ return function () {
+ return static::$v;
+ };
+ }
+
+ public static function getStaticClosure() {
+ return static fn () => static::$v;
+ }
+
+ public static function push($x) {
+ static::$arr[] = $x;
+ }
+
+ public static function has() {
+ return isset(static::$v);
+ }
+
+ public static function getTyped() {
+ return static::$typed;
+ }
+}
+
+// Used for the shared cache slot testing
+class B extends A {
+ public static $v = 'B';
+ public static array $arr = [];
+ public static $t = 'traitB';
+}
+
+$out = [];
+for ($i = 0; $i < 8; $i++) {
+ $o = $i % 2 ? new B : new A;
+ $c = $i % 2 ? 'B' : 'A';
+ $out[] = $c::getStatic() . $o->getInstance() . ($o->getClosure())() . ($c::getStaticClosure())() . $o->getT() . (int) $c::has();
+ $c::push($i);
+}
+echo implode(',', $out), "\n";
+print_r(A::$arr);
+echo "\n";
+print_r(B::$arr);
+echo "\n";
+
+for ($i = 0; $i < 2; $i++) {
+ try {
+ A::getTyped();
+ } catch (Error $e) {
+ echo $e->getMessage(), "\n";
+ }
+}
+A::$typed = 42;
+var_dump(A::getTyped(), B::getTyped());
+
+?>
+--EXPECT--
+AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1,AAAAtrait1,BBBBtraitB1
+Array
+(
+ [0] => 0
+ [1] => 2
+ [2] => 4
+ [3] => 6
+)
+
+Array
+(
+ [0] => 1
+ [1] => 3
+ [2] => 5
+ [3] => 7
+)
+
+Typed static property A::$typed must not be accessed before initialization
+Typed static property A::$typed must not be accessed before initialization
+int(42)
+int(42)