Commit 94136cf9bfb for php.net
commit 94136cf9bfbab89e072e3f04992c1e16c3a981a1
Author: Holly Schilling <holly.a.schilling@outlook.com>
Date: Tue Aug 4 08:19:12 2026 -0500
Improve asymmetric visibility write performance in fast path (GH-22709)
Skip the visibility check in the fast path of ZEND_ASSIGN_OBJ when the cache slot is primed. For this to work, ZEND_FETCH_OBJ_R and the other object handlers must not share a cache slot with ZEND_ASSIGN_OBJ. Also reset the cache slot when the visibility check has failed in the slow path.
Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c
index d603fe159e2..e43da5892b9 100644
--- a/Zend/Optimizer/compact_literals.c
+++ b/Zend/Optimizer/compact_literals.c
@@ -120,7 +120,7 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
HashTable hash;
zend_string *key = NULL;
void *checkpoint = zend_arena_checkpoint(ctx->arena);
- int *const_slot, *class_slot, *func_slot, *bind_var_slot, *property_slot, *method_slot, *jmp_slot;
+ int *const_slot, *class_slot, *func_slot, *bind_var_slot, *property_slot, *method_slot, *jmp_slot, *assign_obj_slots;
if (op_array->last_literal) {
uint32_t j;
@@ -439,14 +439,15 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
zend_hash_clean(&hash);
op_array->last_literal = j;
- const_slot = zend_arena_alloc(&ctx->arena, j * 7 * sizeof(int));
- memset(const_slot, -1, j * 7 * sizeof(int));
+ const_slot = zend_arena_alloc(&ctx->arena, j * 8 * sizeof(int));
+ memset(const_slot, -1, j * 8 * sizeof(int));
class_slot = const_slot + j;
func_slot = class_slot + j;
bind_var_slot = func_slot + j;
property_slot = bind_var_slot + j;
method_slot = property_slot + j;
jmp_slot = method_slot + j;
+ assign_obj_slots = jmp_slot + j;
/* Update opcodes to use new literals table */
cache_size = zend_op_array_extension_handles * sizeof(void*);
@@ -500,6 +501,19 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
}
break;
case ZEND_ASSIGN_OBJ:
+ if (opline->op2_type == IS_CONST) {
+ if (opline->op1_type == IS_UNUSED &&
+ assign_obj_slots[opline->op2.constant] >= 0) {
+ opline->extended_value = assign_obj_slots[opline->op2.constant];
+ } else {
+ opline->extended_value = cache_size;
+ cache_size += 3 * sizeof(void *);
+ if (opline->op1_type == IS_UNUSED) {
+ assign_obj_slots[opline->op2.constant] = opline->extended_value;
+ }
+ }
+ }
+ break;
case ZEND_ASSIGN_OBJ_REF:
case ZEND_FETCH_OBJ_R:
case ZEND_FETCH_OBJ_W:
diff --git a/Zend/tests/asymmetric_visibility/optimizer_shared_cache_slot.phpt b/Zend/tests/asymmetric_visibility/optimizer_shared_cache_slot.phpt
new file mode 100644
index 00000000000..2e24ad1f8d0
--- /dev/null
+++ b/Zend/tests/asymmetric_visibility/optimizer_shared_cache_slot.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Asymmetric set visibility survives optimizer cache-slot sharing between $this reads and writes
+--FILE--
+<?php
+class P {
+ public private(set) string $prop = 'default';
+}
+
+class C extends P {
+ public function test() {
+ // The read populates a runtime cache slot for $this->prop; the write
+ // below must not reuse that (read-kind) resolution to bypass the
+ // set-visibility check when the optimizer shares property slots.
+ var_dump($this->prop);
+ try {
+ $this->prop = 'overwritten';
+ } catch (Error $e) {
+ echo $e->getMessage(), "\n";
+ }
+ var_dump($this->prop);
+ }
+}
+
+$c = new C;
+$c->test();
+$c->test();
+?>
+--EXPECT--
+string(7) "default"
+Cannot modify private(set) property P::$prop from scope C
+string(7) "default"
+string(7) "default"
+Cannot modify private(set) property P::$prop from scope C
+string(7) "default"
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 3af19df4645..ab371cbc4e2 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -1065,7 +1065,7 @@ ZEND_API bool zend_never_inline zend_verify_property_type(const zend_property_in
return i_zend_verify_property_type(info, property, strict);
}
-static zend_never_inline zval* zend_assign_to_typed_prop(const zend_property_info *info, zval *property_val, zval *value, zend_refcounted **garbage_ptr EXECUTE_DATA_DC)
+static zend_never_inline zval* zend_assign_to_typed_prop(const zend_property_info *info, zval *property_val, zval *value, zend_refcounted **garbage_ptr, bool check_writable EXECUTE_DATA_DC)
{
zval tmp;
@@ -1074,7 +1074,7 @@ static zend_never_inline zval* zend_assign_to_typed_prop(const zend_property_inf
zend_readonly_property_modification_error(info);
return &EG(uninitialized_zval);
}
- if (info->flags & ZEND_ACC_PPP_SET_MASK && !zend_asymmetric_property_has_set_access(info)) {
+ if (check_writable && (info->flags & ZEND_ACC_PPP_SET_MASK) && !zend_asymmetric_property_has_set_access(info)) {
zend_asymmetric_visibility_property_modification_error(info, "modify");
return &EG(uninitialized_zval);
}
diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c
index 754f603100c..e06287895d1 100644
--- a/Zend/zend_object_handlers.c
+++ b/Zend/zend_object_handlers.c
@@ -1138,6 +1138,11 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva
if ((prop_info->flags & ZEND_ACC_PPP_SET_MASK) && !zend_asymmetric_property_has_set_access(prop_info)) {
zend_asymmetric_visibility_property_modification_error(prop_info, "modify");
variable_ptr = &EG(error_zval);
+ if (cache_slot) {
+ /* Reset cache slot to dodge fast path in next execution. */
+ CACHE_POLYMORPHIC_PTR_EX(cache_slot, NULL, NULL);
+ CACHE_PTR_EX(cache_slot + 2, NULL);
+ }
goto exit;
}
}
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index d14230514b3..d89d460d9d9 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -2524,7 +2524,7 @@ ZEND_VM_C_LABEL(assign_obj_simple):
property_val = OBJ_PROP(zobj, prop_offset);
if (Z_TYPE_P(property_val) != IS_UNDEF) {
if (prop_info != NULL) {
- value = zend_assign_to_typed_prop(prop_info, property_val, value, &garbage EXECUTE_DATA_CC);
+ value = zend_assign_to_typed_prop(prop_info, property_val, value, &garbage, /* check_writable */ false EXECUTE_DATA_CC);
ZEND_VM_C_GOTO(free_and_exit_assign_obj);
} else {
ZEND_VM_C_LABEL(fast_assign_obj):
@@ -2659,7 +2659,7 @@ ZEND_VM_HANDLER(25, ZEND_ASSIGN_STATIC_PROP, ANY, ANY, CACHE_SLOT, SPEC(OP_DATA=
value = GET_OP_DATA_ZVAL_PTR(BP_VAR_R);
if (ZEND_TYPE_IS_SET(prop_info->type)) {
- value = zend_assign_to_typed_prop(prop_info, prop, value, &garbage EXECUTE_DATA_CC);
+ value = zend_assign_to_typed_prop(prop_info, prop, value, &garbage, /* check_writable */ true EXECUTE_DATA_CC);
FREE_OP_DATA();
} else {
value = zend_assign_to_variable_ex(prop, value, OP_DATA_TYPE, EX_USES_STRICT_TYPES(), &garbage);
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 53bcdccd971..4c830f203bf 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ