Skip to content

Commit 4c71a29

Browse files
committed
[RFC] OPcache Static Cache Implementation
https://wiki.php.net/rfc/opcache_static_cache
1 parent 0078a27 commit 4c71a29

194 files changed

Lines changed: 35592 additions & 175 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
--TEST--
2+
ASSIGN_OBJ_REF tracks object mutations before freeing operands
3+
--SKIPIF--
4+
<?php
5+
$zendDir = dirname(__DIR__);
6+
if (!is_file($zendDir . '/zend_vm_def.h') || !is_file($zendDir . '/zend_vm_execute.h')) {
7+
die('skip source tree required');
8+
}
9+
?>
10+
--FILE--
11+
<?php
12+
13+
function assertAssignObjRefTrackingOrder(string $section, string $label): bool
14+
{
15+
$trackPos = strpos($section, 'ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj);');
16+
$freeNeedles = [
17+
'FREE_OP1();',
18+
'FREE_OP2();',
19+
'FREE_OP_DATA();',
20+
'zval_ptr_dtor_nogc(EX_VAR(opline->op1.var));',
21+
'zval_ptr_dtor_nogc(EX_VAR(opline->op2.var));',
22+
'zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var));',
23+
];
24+
$freePositions = [];
25+
26+
foreach ($freeNeedles as $needle) {
27+
$pos = strpos($section, $needle);
28+
if ($pos !== false) {
29+
$freePositions[] = $pos;
30+
}
31+
}
32+
33+
if ($trackPos === false) {
34+
throw new RuntimeException($label . ' is missing the expected ASSIGN_OBJ_REF sequence');
35+
}
36+
37+
if ($freePositions === []) {
38+
return false;
39+
}
40+
41+
if ($trackPos > min($freePositions)) {
42+
throw new RuntimeException($label . ' tracks object mutation after freeing operands');
43+
}
44+
45+
return true;
46+
}
47+
48+
$zendDir = dirname(__DIR__);
49+
50+
$vmDef = file_get_contents($zendDir . '/zend_vm_def.h');
51+
$defStart = strpos($vmDef, 'ZEND_VM_HANDLER(32, ZEND_ASSIGN_OBJ_REF');
52+
$defEnd = strpos($vmDef, 'ZEND_VM_HANDLER(33, ZEND_ASSIGN_STATIC_PROP_REF');
53+
if ($defStart === false || $defEnd === false) {
54+
throw new RuntimeException('Unable to locate ZEND_ASSIGN_OBJ_REF in zend_vm_def.h');
55+
}
56+
57+
if (!assertAssignObjRefTrackingOrder(substr($vmDef, $defStart, $defEnd - $defStart), 'zend_vm_def.h')) {
58+
throw new RuntimeException('zend_vm_def.h ASSIGN_OBJ_REF unexpectedly has no operand frees');
59+
}
60+
61+
$vmExecute = file_get_contents($zendDir . '/zend_vm_execute.h');
62+
$sections = preg_split('/(?=static ZEND_OPCODE_HANDLER_RET )/', $vmExecute);
63+
$checked = 0;
64+
65+
foreach ($sections as $section) {
66+
$headerEnd = strpos($section, "\n");
67+
$label = $headerEnd === false ? 'zend_vm_execute.h ASSIGN_OBJ_REF handler' : trim(substr($section, 0, $headerEnd));
68+
if (!preg_match('/\bZEND_ASSIGN_OBJ_REF_[A-Z0-9_]+_HANDLER\b/', $label)) {
69+
continue;
70+
}
71+
72+
if (assertAssignObjRefTrackingOrder($section, $label)) {
73+
$checked++;
74+
}
75+
}
76+
77+
if ($checked === 0) {
78+
throw new RuntimeException('Unable to locate generated ZEND_ASSIGN_OBJ_REF handlers in zend_vm_execute.h');
79+
}
80+
81+
echo "OK\n";
82+
83+
?>
84+
--EXPECT--
85+
OK
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
--TEST--
2+
Tracked mutation hooks are called through defensive helpers
3+
--SKIPIF--
4+
<?php
5+
$zendDir = dirname(__DIR__);
6+
foreach (['zend_execute.h', 'zend_execute.c', 'zend_vm_def.h', 'zend_vm_execute.h'] as $file) {
7+
if (!is_file($zendDir . '/' . $file)) {
8+
die('skip source tree required');
9+
}
10+
}
11+
?>
12+
--FILE--
13+
<?php
14+
15+
function sourceSection(string $source, string $start, string $end, string $label): string
16+
{
17+
$startPos = strpos($source, $start);
18+
$endPos = strpos($source, $end, $startPos === false ? 0 : $startPos);
19+
20+
if ($startPos === false || $endPos === false || $endPos <= $startPos) {
21+
throw new RuntimeException('Unable to locate ' . $label);
22+
}
23+
24+
return substr($source, $startPos, $endPos - $startPos);
25+
}
26+
27+
$zendDir = dirname(__DIR__);
28+
$executeH = file_get_contents($zendDir . '/zend_execute.h');
29+
$executeC = file_get_contents($zendDir . '/zend_execute.c');
30+
31+
if (!str_contains($executeH, 'static zend_always_inline bool zend_maybe_track_hash_mutation(HashTable *ht, bool publish)')) {
32+
throw new RuntimeException('Missing hash mutation helper');
33+
}
34+
35+
if (!str_contains($executeH, 'zend_tracked_hash_mutation_hook != NULL')) {
36+
throw new RuntimeException('Hash mutation helper does not guard the hook pointer');
37+
}
38+
39+
foreach (['zend_execute.c', 'zend_vm_def.h', 'zend_vm_execute.h'] as $file) {
40+
$source = file_get_contents($zendDir . '/' . $file);
41+
if (str_contains($source, 'zend_tracked_hash_mutation_hook(')) {
42+
throw new RuntimeException($file . ' calls zend_tracked_hash_mutation_hook() directly');
43+
}
44+
}
45+
46+
if (!str_contains($executeC, 'zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL')) {
47+
throw new RuntimeException('Object mutation slow path does not guard the hook pointer');
48+
}
49+
50+
if (!str_contains($executeC, 'zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL && value != &EG(error_zval)')) {
51+
throw new RuntimeException('Object mutation with-value slow path does not guard the hook pointer');
52+
}
53+
54+
$assign = sourceSection(
55+
$executeH,
56+
'static zend_always_inline zval* zend_assign_to_variable(',
57+
'static zend_always_inline zval* zend_assign_to_variable_ex(',
58+
'zend_assign_to_variable()'
59+
);
60+
if (substr_count($assign, 'zend_maybe_track_reference_update(updated_ref);') < 2) {
61+
throw new RuntimeException('zend_assign_to_variable() does not notify reference updates');
62+
}
63+
64+
$assignEx = sourceSection(
65+
$executeH,
66+
'static zend_always_inline zval* zend_assign_to_variable_ex(',
67+
'static zend_always_inline void zend_class_static_update(',
68+
'zend_assign_to_variable_ex()'
69+
);
70+
if (substr_count($assignEx, 'zend_maybe_track_reference_update(updated_ref);') < 2) {
71+
throw new RuntimeException('zend_assign_to_variable_ex() does not notify reference updates');
72+
}
73+
74+
echo "OK\n";
75+
76+
?>
77+
--EXPECT--
78+
OK

Zend/zend.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ ZEND_API zend_string *(*zend_resolve_path)(zend_string *filename);
9696
ZEND_API zend_result (*zend_post_startup_cb)(void) = NULL;
9797
ZEND_API void (*zend_post_shutdown_cb)(void) = NULL;
9898
ZEND_API void (*zend_accel_schedule_restart_hook)(int reason) = NULL;
99+
ZEND_API void (*zend_class_init_statics_hook)(zend_class_entry *ce) = NULL;
100+
ZEND_API void (*zend_function_init_statics_hook)(zend_execute_data *execute_data) = NULL;
101+
ZEND_API void (*zend_class_static_access_hook)(zend_class_entry *ce) = NULL;
102+
ZEND_API void (*zend_class_static_update_hook)(zend_class_entry *ce) = NULL;
103+
ZEND_API void (*zend_tracked_reference_update_hook)(zend_reference *ref) = NULL;
104+
ZEND_API bool (*zend_tracked_hash_mutation_hook)(HashTable *ht, bool publish) = NULL;
105+
ZEND_API void (*zend_tracked_object_mutation_hook)(zend_object *obj) = NULL;
99106
ZEND_ATTRIBUTE_NONNULL ZEND_API zend_result (*zend_random_bytes)(void *bytes, size_t size, char *errstr, size_t errstr_size) = NULL;
100107
ZEND_ATTRIBUTE_NONNULL ZEND_API void (*zend_random_bytes_insecure)(zend_random_bytes_insecure_state *state, void *bytes, size_t size) = NULL;
101108

@@ -819,6 +826,8 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
819826
#endif
820827
executor_globals->saved_fpu_cw_ptr = NULL;
821828
executor_globals->active = false;
829+
executor_globals->static_cache_class_access_active = false;
830+
executor_globals->tracked_mutation_hooks_active = false;
822831
executor_globals->bailout = NULL;
823832
executor_globals->error_handling = EH_NORMAL;
824833
executor_globals->exception_class = NULL;

Zend/zend.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,17 @@ extern ZEND_API void (*zend_post_shutdown_cb)(void);
386386

387387
extern ZEND_API void (*zend_accel_schedule_restart_hook)(int reason);
388388

389+
/* These hooks are used by OPcache Static Cache to restore, publish, and track
390+
* selected VolatileStatic and PersistentStatic state across requests. They remain
391+
* NULL when the static-cache subsystem is not active. */
392+
extern ZEND_API void (*zend_class_init_statics_hook)(zend_class_entry *ce);
393+
extern ZEND_API void (*zend_function_init_statics_hook)(zend_execute_data *execute_data);
394+
extern ZEND_API void (*zend_class_static_access_hook)(zend_class_entry *ce);
395+
extern ZEND_API void (*zend_class_static_update_hook)(zend_class_entry *ce);
396+
extern ZEND_API void (*zend_tracked_reference_update_hook)(zend_reference *ref);
397+
extern ZEND_API bool (*zend_tracked_hash_mutation_hook)(HashTable *ht, bool publish);
398+
extern ZEND_API void (*zend_tracked_object_mutation_hook)(zend_object *obj);
399+
389400
ZEND_API ZEND_COLD void zend_error(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
390401
ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3);
391402
ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn_unchecked(int type, const char *format, ...);

Zend/zend_execute.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2908,6 +2908,11 @@ static zend_always_inline void zend_fetch_dimension_address(zval *result, zval *
29082908

29092909
if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) {
29102910
try_array:
2911+
if (UNEXPECTED((type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) &&
2912+
EG(tracked_mutation_hooks_active))
2913+
) {
2914+
zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false);
2915+
}
29112916
SEPARATE_ARRAY(container);
29122917
fetch_from_array:
29132918
if (dim == NULL) {
@@ -3507,6 +3512,20 @@ static zend_never_inline bool zend_handle_fetch_obj_flags(
35073512
return 1;
35083513
}
35093514

3515+
static zend_always_inline void zend_track_property_array_indirect_mutation(zval *ptr, int type)
3516+
{
3517+
zval *tracked = ptr;
3518+
3519+
if (UNEXPECTED((type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) &&
3520+
EG(tracked_mutation_hooks_active))
3521+
) {
3522+
ZVAL_DEREF(tracked);
3523+
if (Z_TYPE_P(tracked) == IS_ARRAY) {
3524+
zend_maybe_track_hash_mutation(Z_ARRVAL_P(tracked), false);
3525+
}
3526+
}
3527+
}
3528+
35103529
static zend_always_inline void zend_fetch_property_address(
35113530
zval *result,
35123531
const zval *container,
@@ -3591,6 +3610,7 @@ static zend_always_inline void zend_fetch_property_address(
35913610
zend_handle_fetch_obj_flags(result, ptr, NULL, prop_info, flags);
35923611
}
35933612
}
3613+
zend_track_property_array_indirect_mutation(ptr, type);
35943614
return;
35953615
}
35963616
} else if (UNEXPECTED(IS_HOOKED_PROPERTY_OFFSET(prop_offset))) {
@@ -3606,6 +3626,7 @@ static zend_always_inline void zend_fetch_property_address(
36063626
ptr = zend_hash_find_known_hash(zobj->properties, Z_STR_P(prop_ptr));
36073627
if (EXPECTED(ptr)) {
36083628
ZVAL_INDIRECT(result, ptr);
3629+
zend_track_property_array_indirect_mutation(ptr, type);
36093630
return;
36103631
}
36113632
}
@@ -3653,6 +3674,7 @@ static zend_always_inline void zend_fetch_property_address(
36533674
}
36543675
}
36553676
}
3677+
zend_track_property_array_indirect_mutation(ptr, type);
36563678

36573679
end:
36583680
if (prop_info_p) {
@@ -3828,6 +3850,12 @@ static zend_always_inline zval* zend_fetch_static_property_address(zend_property
38283850
result = CACHED_PTR(cache_slot + sizeof(void *));
38293851
property_info = CACHED_PTR(cache_slot + sizeof(void *) * 2);
38303852

3853+
if (UNEXPECTED(EG(static_cache_class_access_active) &&
3854+
zend_class_static_access_hook != NULL)
3855+
) {
3856+
zend_class_static_access_hook(property_info->ce);
3857+
}
3858+
38313859
if ((fetch_type == BP_VAR_R || fetch_type == BP_VAR_RW)
38323860
&& UNEXPECTED(Z_TYPE_P(result) == IS_UNDEF)
38333861
&& ZEND_TYPE_IS_SET(property_info->type)) {
@@ -4102,6 +4130,20 @@ ZEND_API zval* zend_assign_to_typed_ref(zval *variable_ptr, zval *orig_value, ui
41024130
return result;
41034131
}
41044132

4133+
zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_slow(zend_object *zobj)
4134+
{
4135+
if (zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL) {
4136+
zend_tracked_object_mutation_hook(zobj);
4137+
}
4138+
}
4139+
4140+
zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_with_value_slow(zend_object *zobj, zval *value)
4141+
{
4142+
if (zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL && value != &EG(error_zval)) {
4143+
zend_tracked_object_mutation_hook(zobj);
4144+
}
4145+
}
4146+
41054147
ZEND_API bool ZEND_FASTCALL zend_verify_prop_assignable_by_ref_ex(const zend_property_info *prop_info, zval *orig_val, bool strict, zend_verify_prop_assignable_by_ref_context context) {
41064148
zval *val = orig_val;
41074149
if (Z_ISREF_P(val) && ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(val))) {

0 commit comments

Comments
 (0)