From b45bae9cca9f2931c47a0a7ba241321fb964763c Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 17 Sep 2025 19:18:05 +0200 Subject: [PATCH 01/10] Add fcc cache Fixes GH-19754 --- .../constexpr/namespace_004.phpt | 8 ++--- Zend/zend_compile.c | 15 +++++++- Zend/zend_execute_API.c | 14 ++++++++ Zend/zend_globals.h | 2 ++ Zend/zend_vm_def.h | 19 +++++++++-- Zend/zend_vm_execute.h | 34 +++++++++++++++++-- Zend/zend_vm_opcodes.c | 2 +- 7 files changed, 84 insertions(+), 10 deletions(-) diff --git a/Zend/tests/first_class_callable/constexpr/namespace_004.phpt b/Zend/tests/first_class_callable/constexpr/namespace_004.phpt index 0fc23422199d5..6fe99593bf95d 100644 --- a/Zend/tests/first_class_callable/constexpr/namespace_004.phpt +++ b/Zend/tests/first_class_callable/constexpr/namespace_004.phpt @@ -26,7 +26,7 @@ foo(); ?> --EXPECTF-- -object(Closure)#1 (2) { +object(Closure)#%d (2) { ["function"]=> string(6) "strrev" ["parameter"]=> @@ -36,7 +36,7 @@ object(Closure)#1 (2) { } } string(3) "cba" -object(Closure)#2 (2) { +object(Closure)#%d (2) { ["function"]=> string(6) "strrev" ["parameter"]=> @@ -46,7 +46,7 @@ object(Closure)#2 (2) { } } string(3) "cba" -object(Closure)#2 (2) { +object(Closure)#%d (2) { ["function"]=> string(6) "strrev" ["parameter"]=> @@ -56,7 +56,7 @@ object(Closure)#2 (2) { } } string(3) "cba" -object(Closure)#1 (2) { +object(Closure)#%d (2) { ["function"]=> string(6) "strrev" ["parameter"]=> diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index b46b5d5bff8d2..de34981b829ff 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3964,7 +3964,20 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, zend_fun opline->op1.num = zend_vm_calc_used_stack(0, fbc); } - zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL); + zend_op *callable_convert_op = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL); + /* main is not expected to run very often. */ + if (CG(active_op_array)->function_name && (opline->opcode == ZEND_INIT_FCALL + || opline->opcode == ZEND_INIT_FCALL_BY_NAME + || opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME + || (opline->opcode == ZEND_INIT_STATIC_METHOD_CALL + && (opline->op1_type == IS_CONST + /* Closures may change scope, hence don't cache callables. */ + || (opline->op1_type == IS_UNUSED && !(CG(active_op_array)->fn_flags & ZEND_ACC_CLOSURE))) + && opline->op2_type == IS_CONST))) { + callable_convert_op->extended_value = zend_alloc_cache_slot(); + } else { + callable_convert_op->extended_value = (uint32_t)-1; + } return true; } diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 91b8c5ab210ef..f75d453cd1789 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -203,6 +203,8 @@ void init_executor(void) /* {{{ */ zend_fiber_init(); zend_weakrefs_init(); + zend_stack_init(&EG(callable_convert_cache), sizeof(zend_object*)); + EG(active) = 1; } /* }}} */ @@ -267,6 +269,14 @@ void shutdown_destructors(void) /* {{{ */ } /* }}} */ +void callable_convert_dtor(zend_object **object_ptr) +{ + zend_object *object = *object_ptr; + if (zend_gc_delref(&object->gc) == 0) { + zend_objects_store_del(object); + } +} + /* Free values held by the executor. */ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown) { @@ -420,6 +430,8 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown) zend_stack_clean(&EG(user_error_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1); zend_stack_clean(&EG(user_exception_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1); + zend_stack_clean(&EG(callable_convert_cache), (void (*)(void *))callable_convert_dtor, 1); + #if ZEND_DEBUG if (!CG(unclean_shutdown)) { gc_collect_cycles(); @@ -516,6 +528,8 @@ void shutdown_executor(void) /* {{{ */ if (EG(ht_iterators) != EG(ht_iterators_slots)) { efree(EG(ht_iterators)); } + + zend_stack_destroy(&EG(callable_convert_cache)); } #if ZEND_DEBUG diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 48b978b535014..1af62a625daf5 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -319,6 +319,8 @@ struct _zend_executor_globals { zend_strtod_state strtod_state; + zend_stack callable_convert_cache; + void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 3ebf816cf3817..8140e45bf7a8e 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9709,12 +9709,27 @@ ZEND_VM_HANDLER(167, ZEND_COPY_TMP, TMPVAR, UNUSED) ZEND_VM_NEXT_OPCODE(); } -ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED) +ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED, NUM|CACHE_SLOT) { USE_OPLINE zend_execute_data *call = EX(call); - zend_closure_from_frame(EX_VAR(opline->result.var), call); + if (opline->extended_value != (uint32_t)-1) { + int offset = (int)(uintptr_t)CACHED_PTR(opline->extended_value); + if (offset) { + offset--; + zend_object *closure = ((zend_object**)zend_stack_base(&EG(callable_convert_cache)))[offset]; + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var)); + GC_ADDREF(closure); + /* Offset by 1 to free 0 as empty sentinel. */ + CACHE_PTR(opline->extended_value, (void*)(uintptr_t)(zend_stack_push(&EG(callable_convert_cache), &closure) + 1)); + } + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index b0d6f2bc33d96..d2a493fb36129 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -39087,7 +39087,22 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV USE_OPLINE zend_execute_data *call = EX(call); - zend_closure_from_frame(EX_VAR(opline->result.var), call); + if (opline->extended_value != (uint32_t)-1) { + int offset = (int)(uintptr_t)CACHED_PTR(opline->extended_value); + if (offset) { + offset--; + zend_object *closure = ((zend_object**)zend_stack_base(&EG(callable_convert_cache)))[offset]; + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var)); + GC_ADDREF(closure); + /* Offset by 1 to free 0 as empty sentinel. */ + CACHE_PTR(opline->extended_value, (void*)(uintptr_t)(zend_stack_push(&EG(callable_convert_cache), &closure) + 1)); + } + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); @@ -94308,7 +94323,22 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_S USE_OPLINE zend_execute_data *call = EX(call); - zend_closure_from_frame(EX_VAR(opline->result.var), call); + if (opline->extended_value != (uint32_t)-1) { + int offset = (int)(uintptr_t)CACHED_PTR(opline->extended_value); + if (offset) { + offset--; + zend_object *closure = ((zend_object**)zend_stack_base(&EG(callable_convert_cache)))[offset]; + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var)); + GC_ADDREF(closure); + /* Offset by 1 to free 0 as empty sentinel. */ + CACHE_PTR(opline->extended_value, (void*)(uintptr_t)(zend_stack_push(&EG(callable_convert_cache), &closure) + 1)); + } + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + } if (ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS) { OBJ_RELEASE(Z_OBJ(call->This)); diff --git a/Zend/zend_vm_opcodes.c b/Zend/zend_vm_opcodes.c index 00ad38baaafeb..936a96e55e41f 100644 --- a/Zend/zend_vm_opcodes.c +++ b/Zend/zend_vm_opcodes.c @@ -439,7 +439,7 @@ static uint32_t zend_vm_opcodes_flags[211] = { 0x00000101, 0x00000101, 0x00000101, - 0x00000101, + 0x01040101, 0x00002001, 0x00000101, 0x00000100, From 34375abe7c0f50cd7f3b19c40b8e6a8d4fe0be00 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 17 Sep 2025 22:50:25 +0200 Subject: [PATCH 02/10] Fix for optimizer, re-enable for main --- Zend/Optimizer/compact_literals.c | 6 ++++++ .../first_class_callable_optimization.phpt | 6 +++--- .../trampoline_closure_named_arguments.phpt | 14 +++++++------- Zend/zend_compile.c | 5 ++--- ext/opcache/tests/jit/opcache_jit_blacklist.phpt | 3 +-- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c index d0aaccec7ce2c..9c30fae9e4e7b 100644 --- a/Zend/Optimizer/compact_literals.c +++ b/Zend/Optimizer/compact_literals.c @@ -740,6 +740,12 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx cache_size += 2 * sizeof(void *); } break; + case ZEND_CALLABLE_CONVERT: + if (opline->extended_value != (uint32_t)-1) { + opline->extended_value = cache_size; + cache_size += sizeof(void *); + } + break; } opline++; } diff --git a/Zend/tests/first_class_callable/first_class_callable_optimization.phpt b/Zend/tests/first_class_callable/first_class_callable_optimization.phpt index 707b6a7299a4a..169f6dfd62cc2 100644 --- a/Zend/tests/first_class_callable/first_class_callable_optimization.phpt +++ b/Zend/tests/first_class_callable/first_class_callable_optimization.phpt @@ -10,12 +10,12 @@ var_dump(test1(...)); var_dump(test2(...)); ?> ---EXPECT-- -object(Closure)#1 (1) { +--EXPECTF-- +object(Closure)#%d (1) { ["function"]=> string(5) "test1" } -object(Closure)#1 (1) { +object(Closure)#%d (1) { ["function"]=> string(5) "test2" } diff --git a/Zend/tests/magic_methods/trampoline_closure_named_arguments.phpt b/Zend/tests/magic_methods/trampoline_closure_named_arguments.phpt index e4ccaf16e63a6..3be14f6145ace 100644 --- a/Zend/tests/magic_methods/trampoline_closure_named_arguments.phpt +++ b/Zend/tests/magic_methods/trampoline_closure_named_arguments.phpt @@ -42,7 +42,7 @@ var_dump($type); var_dump($type->getName()); ?> ---EXPECT-- +--EXPECTF-- -- Non-static cases -- string(4) "test" array(3) { @@ -69,7 +69,7 @@ array(4) { ["a"]=> int(123) ["b"]=> - object(Test)#1 (0) { + object(Test)#%d (0) { } } string(4) "test" @@ -77,7 +77,7 @@ array(2) { ["a"]=> int(123) ["b"]=> - object(Test)#1 (0) { + object(Test)#%d (0) { } } string(4) "test" @@ -114,7 +114,7 @@ array(4) { ["a"]=> int(123) ["b"]=> - object(Test)#1 (0) { + object(Test)#%d (0) { } } string(10) "testStatic" @@ -122,7 +122,7 @@ array(2) { ["a"]=> int(123) ["b"]=> - object(Test)#1 (0) { + object(Test)#%d (0) { } } string(10) "testStatic" @@ -136,12 +136,12 @@ array(1) { -- Reflection tests -- array(1) { [0]=> - object(ReflectionParameter)#4 (1) { + object(ReflectionParameter)#%d (1) { ["name"]=> string(9) "arguments" } } bool(true) -object(ReflectionNamedType)#5 (0) { +object(ReflectionNamedType)#%d (0) { } string(5) "mixed" diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index de34981b829ff..64146214ab5ff 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3965,15 +3965,14 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, zend_fun } zend_op *callable_convert_op = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL); - /* main is not expected to run very often. */ - if (CG(active_op_array)->function_name && (opline->opcode == ZEND_INIT_FCALL + if (opline->opcode == ZEND_INIT_FCALL || opline->opcode == ZEND_INIT_FCALL_BY_NAME || opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME || (opline->opcode == ZEND_INIT_STATIC_METHOD_CALL && (opline->op1_type == IS_CONST /* Closures may change scope, hence don't cache callables. */ || (opline->op1_type == IS_UNUSED && !(CG(active_op_array)->fn_flags & ZEND_ACC_CLOSURE))) - && opline->op2_type == IS_CONST))) { + && opline->op2_type == IS_CONST)) { callable_convert_op->extended_value = zend_alloc_cache_slot(); } else { callable_convert_op->extended_value = (uint32_t)-1; diff --git a/ext/opcache/tests/jit/opcache_jit_blacklist.phpt b/ext/opcache/tests/jit/opcache_jit_blacklist.phpt index 33db720967555..1ac97fbe53e9b 100644 --- a/ext/opcache/tests/jit/opcache_jit_blacklist.phpt +++ b/ext/opcache/tests/jit/opcache_jit_blacklist.phpt @@ -16,8 +16,7 @@ function foo() { ++$x; var_dump($x); } -opcache_jit_blacklist(foo(...)); -foo(); +foo(...)(); ?> --EXPECT-- int(2) From ec7402dfb36cf3459036fcdbc565e999ccd272fb Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 17 Sep 2025 23:39:18 +0200 Subject: [PATCH 03/10] Fix dom test --- ext/dom/tests/registerPhpFunctionNS.phpt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/dom/tests/registerPhpFunctionNS.phpt b/ext/dom/tests/registerPhpFunctionNS.phpt index 4c4fb157000bf..2ee98168251fd 100644 --- a/ext/dom/tests/registerPhpFunctionNS.phpt +++ b/ext/dom/tests/registerPhpFunctionNS.phpt @@ -62,14 +62,14 @@ $xpath->registerPhpFunctionNS('urn:bar', 'test', 'strtolower'); var_dump($xpath->query('//a[bar:test(string(@href)) = "https://php.net"]')); ?> ---EXPECT-- +--EXPECTF-- --- Legit cases: global function callable --- object(DOMNodeList)#5 (1) { ["length"]=> int(1) } --- Legit cases: string callable --- -object(DOMNodeList)#5 (1) { +object(DOMNodeList)#%d (1) { ["length"]=> int(1) } @@ -79,12 +79,12 @@ array(1) { [0]=> string(15) "https://PHP.net" } -object(DOMNodeList)#3 (1) { +object(DOMNodeList)#%d (1) { ["length"]=> int(0) } --- Legit cases: instance class method callable --- -object(DOMNodeList)#6 (1) { +object(DOMNodeList)#%d (1) { ["length"]=> int(1) } @@ -100,7 +100,7 @@ array(1) { --- Legit cases: global function callable that returns nothing --- string(15) "https://PHP.net" --- Legit cases: multiple namespaces --- -object(DOMNodeList)#5 (1) { +object(DOMNodeList)#%d (1) { ["length"]=> int(1) } From fb4d6e9cff8e3c06a9acb1c0edaef56dcfa05582 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 22 Sep 2025 11:21:29 +0200 Subject: [PATCH 04/10] Switch to hash table cache --- Zend/zend_execute_API.c | 6 +++--- Zend/zend_globals.h | 2 +- Zend/zend_vm_def.h | 21 ++++++++++++--------- Zend/zend_vm_execute.h | 42 +++++++++++++++++++++++------------------ 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index f75d453cd1789..c300b7cc60be6 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -203,7 +203,7 @@ void init_executor(void) /* {{{ */ zend_fiber_init(); zend_weakrefs_init(); - zend_stack_init(&EG(callable_convert_cache), sizeof(zend_object*)); + zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); EG(active) = 1; } @@ -430,7 +430,7 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown) zend_stack_clean(&EG(user_error_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1); zend_stack_clean(&EG(user_exception_handlers), (void (*)(void *))ZVAL_PTR_DTOR, 1); - zend_stack_clean(&EG(callable_convert_cache), (void (*)(void *))callable_convert_dtor, 1); + zend_hash_clean(&EG(callable_convert_cache)); #if ZEND_DEBUG if (!CG(unclean_shutdown)) { @@ -529,7 +529,7 @@ void shutdown_executor(void) /* {{{ */ efree(EG(ht_iterators)); } - zend_stack_destroy(&EG(callable_convert_cache)); + zend_hash_destroy(&EG(callable_convert_cache)); } #if ZEND_DEBUG diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 1af62a625daf5..fa24128ae20ed 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -319,7 +319,7 @@ struct _zend_executor_globals { zend_strtod_state strtod_state; - zend_stack callable_convert_cache; + HashTable callable_convert_cache; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 8140e45bf7a8e..dac833b211e8d 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9715,17 +9715,20 @@ ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED, NUM|CACHE_SLOT) zend_execute_data *call = EX(call); if (opline->extended_value != (uint32_t)-1) { - int offset = (int)(uintptr_t)CACHED_PTR(opline->extended_value); - if (offset) { - offset--; - zend_object *closure = ((zend_object**)zend_stack_base(&EG(callable_convert_cache)))[offset]; + zend_object *closure = CACHED_PTR(opline->extended_value); + if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - zend_closure_from_frame(EX_VAR(opline->result.var), call); - zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var)); - GC_ADDREF(closure); - /* Offset by 1 to free 0 as empty sentinel. */ - CACHE_PTR(opline->extended_value, (void*)(uintptr_t)(zend_stack_push(&EG(callable_convert_cache), &closure) + 1)); + closure = zend_hash_find_ptr(&EG(callable_convert_cache), call->func->common.function_name); + if (closure) { + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + closure = Z_OBJ_P(EX_VAR(opline->result.var)); + GC_ADDREF(closure); + zend_hash_add_ptr(&EG(callable_convert_cache), call->func->common.function_name, closure); + } + CACHE_PTR(opline->extended_value, closure); } } else { zend_closure_from_frame(EX_VAR(opline->result.var), call); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index d2a493fb36129..7faa2b9d9be83 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -39088,17 +39088,20 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV zend_execute_data *call = EX(call); if (opline->extended_value != (uint32_t)-1) { - int offset = (int)(uintptr_t)CACHED_PTR(opline->extended_value); - if (offset) { - offset--; - zend_object *closure = ((zend_object**)zend_stack_base(&EG(callable_convert_cache)))[offset]; + zend_object *closure = CACHED_PTR(opline->extended_value); + if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - zend_closure_from_frame(EX_VAR(opline->result.var), call); - zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var)); - GC_ADDREF(closure); - /* Offset by 1 to free 0 as empty sentinel. */ - CACHE_PTR(opline->extended_value, (void*)(uintptr_t)(zend_stack_push(&EG(callable_convert_cache), &closure) + 1)); + closure = zend_hash_find_ptr(&EG(callable_convert_cache), call->func->common.function_name); + if (closure) { + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + closure = Z_OBJ_P(EX_VAR(opline->result.var)); + GC_ADDREF(closure); + zend_hash_add_ptr(&EG(callable_convert_cache), call->func->common.function_name, closure); + } + CACHE_PTR(opline->extended_value, closure); } } else { zend_closure_from_frame(EX_VAR(opline->result.var), call); @@ -94324,17 +94327,20 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_S zend_execute_data *call = EX(call); if (opline->extended_value != (uint32_t)-1) { - int offset = (int)(uintptr_t)CACHED_PTR(opline->extended_value); - if (offset) { - offset--; - zend_object *closure = ((zend_object**)zend_stack_base(&EG(callable_convert_cache)))[offset]; + zend_object *closure = CACHED_PTR(opline->extended_value); + if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - zend_closure_from_frame(EX_VAR(opline->result.var), call); - zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var)); - GC_ADDREF(closure); - /* Offset by 1 to free 0 as empty sentinel. */ - CACHE_PTR(opline->extended_value, (void*)(uintptr_t)(zend_stack_push(&EG(callable_convert_cache), &closure) + 1)); + closure = zend_hash_find_ptr(&EG(callable_convert_cache), call->func->common.function_name); + if (closure) { + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); + } else { + zend_closure_from_frame(EX_VAR(opline->result.var), call); + closure = Z_OBJ_P(EX_VAR(opline->result.var)); + GC_ADDREF(closure); + zend_hash_add_ptr(&EG(callable_convert_cache), call->func->common.function_name, closure); + } + CACHE_PTR(opline->extended_value, closure); } } else { zend_closure_from_frame(EX_VAR(opline->result.var), call); From d18d26f9bc1c27ee8266f86b36edb6d2efc374ee Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 22 Sep 2025 11:44:50 +0200 Subject: [PATCH 05/10] Fix destructor --- Zend/zend_execute_API.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index c300b7cc60be6..99335cd17aeef 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -125,6 +125,14 @@ static int clean_non_persistent_class_full(zval *zv) /* {{{ */ } /* }}} */ +static void callable_convert_dtor(zval *object_ptr) +{ + zend_object *object = Z_PTR_P(object_ptr); + if (zend_gc_delref(&object->gc) == 0) { + zend_objects_store_del(object); + } +} + void init_executor(void) /* {{{ */ { zend_init_fpu(); @@ -203,7 +211,7 @@ void init_executor(void) /* {{{ */ zend_fiber_init(); zend_weakrefs_init(); - zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); + zend_hash_init(&EG(callable_convert_cache), 8, NULL, callable_convert_dtor, 0); EG(active) = 1; } @@ -269,14 +277,6 @@ void shutdown_destructors(void) /* {{{ */ } /* }}} */ -void callable_convert_dtor(zend_object **object_ptr) -{ - zend_object *object = *object_ptr; - if (zend_gc_delref(&object->gc) == 0) { - zend_objects_store_del(object); - } -} - /* Free values held by the executor. */ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown) { From 3e93f66e17efdc6556ba3cea580aeb87cd5ea706 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 22 Sep 2025 12:19:26 +0200 Subject: [PATCH 06/10] Fix static methods and tests Don't cache static methods as the function name is not fully qualified (i.e. doesn't contain the class name). --- Zend/tests/exit/exit_as_function.phpt | 6 +++--- Zend/zend_compile.c | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Zend/tests/exit/exit_as_function.phpt b/Zend/tests/exit/exit_as_function.phpt index fb95b9f288171..726cf1f0d7a94 100644 --- a/Zend/tests/exit/exit_as_function.phpt +++ b/Zend/tests/exit/exit_as_function.phpt @@ -19,10 +19,10 @@ foreach ($values as $value) { } ?> ---EXPECT-- +--EXPECTF-- string(4) "exit" string(3) "die" -object(Closure)#1 (2) { +object(Closure)#%d (2) { ["function"]=> string(4) "exit" ["parameter"]=> @@ -31,7 +31,7 @@ object(Closure)#1 (2) { string(10) "" } } -object(Closure)#2 (2) { +object(Closure)#%d (2) { ["function"]=> string(4) "exit" ["parameter"]=> diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 64146214ab5ff..43536061885c8 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3967,12 +3967,7 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, zend_fun zend_op *callable_convert_op = zend_emit_op_tmp(result, ZEND_CALLABLE_CONVERT, NULL, NULL); if (opline->opcode == ZEND_INIT_FCALL || opline->opcode == ZEND_INIT_FCALL_BY_NAME - || opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME - || (opline->opcode == ZEND_INIT_STATIC_METHOD_CALL - && (opline->op1_type == IS_CONST - /* Closures may change scope, hence don't cache callables. */ - || (opline->op1_type == IS_UNUSED && !(CG(active_op_array)->fn_flags & ZEND_ACC_CLOSURE))) - && opline->op2_type == IS_CONST)) { + || opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { callable_convert_op->extended_value = zend_alloc_cache_slot(); } else { callable_convert_op->extended_value = (uint32_t)-1; From f32bb970b9b07fb81c42dc21cd4342405a54e98a Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 22 Sep 2025 12:21:36 +0200 Subject: [PATCH 07/10] Revert accidental change --- ext/opcache/tests/jit/opcache_jit_blacklist.phpt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/opcache/tests/jit/opcache_jit_blacklist.phpt b/ext/opcache/tests/jit/opcache_jit_blacklist.phpt index 1ac97fbe53e9b..33db720967555 100644 --- a/ext/opcache/tests/jit/opcache_jit_blacklist.phpt +++ b/ext/opcache/tests/jit/opcache_jit_blacklist.phpt @@ -16,7 +16,8 @@ function foo() { ++$x; var_dump($x); } -foo(...)(); +opcache_jit_blacklist(foo(...)); +foo(); ?> --EXPECT-- int(2) From bee5b99fd4994b8bbd02e945be3017c5282d6bea Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 24 Sep 2025 00:00:47 +0200 Subject: [PATCH 08/10] Use function ptr as callable cache key --- Zend/zend_vm_def.h | 4 ++-- Zend/zend_vm_execute.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index dac833b211e8d..d24d6ed094759 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9719,14 +9719,14 @@ ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED, NUM|CACHE_SLOT) if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - closure = zend_hash_find_ptr(&EG(callable_convert_cache), call->func->common.function_name); + closure = zend_hash_index_find_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { zend_closure_from_frame(EX_VAR(opline->result.var), call); closure = Z_OBJ_P(EX_VAR(opline->result.var)); GC_ADDREF(closure); - zend_hash_add_ptr(&EG(callable_convert_cache), call->func->common.function_name, closure); + zend_hash_index_add_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func, closure); } CACHE_PTR(opline->extended_value, closure); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 7faa2b9d9be83..16288dba3eb05 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -39092,14 +39092,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - closure = zend_hash_find_ptr(&EG(callable_convert_cache), call->func->common.function_name); + closure = zend_hash_index_find_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { zend_closure_from_frame(EX_VAR(opline->result.var), call); closure = Z_OBJ_P(EX_VAR(opline->result.var)); GC_ADDREF(closure); - zend_hash_add_ptr(&EG(callable_convert_cache), call->func->common.function_name, closure); + zend_hash_index_add_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func, closure); } CACHE_PTR(opline->extended_value, closure); } @@ -94331,14 +94331,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_S if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - closure = zend_hash_find_ptr(&EG(callable_convert_cache), call->func->common.function_name); + closure = zend_hash_index_find_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { zend_closure_from_frame(EX_VAR(opline->result.var), call); closure = Z_OBJ_P(EX_VAR(opline->result.var)); GC_ADDREF(closure); - zend_hash_add_ptr(&EG(callable_convert_cache), call->func->common.function_name, closure); + zend_hash_index_add_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func, closure); } CACHE_PTR(opline->extended_value, closure); } From f8f500605329d5319d7086a9ede2e2aaec2d83d9 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 30 Sep 2025 23:18:27 +0200 Subject: [PATCH 09/10] Use zend_hash_index_lookup() and simplify --- Zend/zend_vm_def.h | 14 ++++++-------- Zend/zend_vm_execute.h | 28 ++++++++++++---------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index d24d6ed094759..1193ed86fe0ed 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -9719,15 +9719,13 @@ ZEND_VM_HANDLER(202, ZEND_CALLABLE_CONVERT, UNUSED, UNUSED, NUM|CACHE_SLOT) if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - closure = zend_hash_index_find_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); - if (closure) { - ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); - } else { - zend_closure_from_frame(EX_VAR(opline->result.var), call); - closure = Z_OBJ_P(EX_VAR(opline->result.var)); - GC_ADDREF(closure); - zend_hash_index_add_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func, closure); + zval *closure_zv = zend_hash_index_lookup(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); + if (Z_TYPE_P(closure_zv) == IS_NULL) { + zend_closure_from_frame(closure_zv, call); } + ZEND_ASSERT(Z_TYPE_P(closure_zv) == IS_OBJECT); + closure = Z_OBJ_P(closure_zv); + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); CACHE_PTR(opline->extended_value, closure); } } else { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 16288dba3eb05..4f42cdc2af9dc 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -39092,15 +39092,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_CALLABLE_CONV if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - closure = zend_hash_index_find_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); - if (closure) { - ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); - } else { - zend_closure_from_frame(EX_VAR(opline->result.var), call); - closure = Z_OBJ_P(EX_VAR(opline->result.var)); - GC_ADDREF(closure); - zend_hash_index_add_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func, closure); + zval *closure_zv = zend_hash_index_lookup(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); + if (Z_TYPE_P(closure_zv) == IS_NULL) { + zend_closure_from_frame(closure_zv, call); } + ZEND_ASSERT(Z_TYPE_P(closure_zv) == IS_OBJECT); + closure = Z_OBJ_P(closure_zv); + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); CACHE_PTR(opline->extended_value, closure); } } else { @@ -94331,15 +94329,13 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_CALLABLE_CONVERT_S if (closure) { ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); } else { - closure = zend_hash_index_find_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); - if (closure) { - ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); - } else { - zend_closure_from_frame(EX_VAR(opline->result.var), call); - closure = Z_OBJ_P(EX_VAR(opline->result.var)); - GC_ADDREF(closure); - zend_hash_index_add_ptr(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func, closure); + zval *closure_zv = zend_hash_index_lookup(&EG(callable_convert_cache), (zend_ulong)(uintptr_t)call->func); + if (Z_TYPE_P(closure_zv) == IS_NULL) { + zend_closure_from_frame(closure_zv, call); } + ZEND_ASSERT(Z_TYPE_P(closure_zv) == IS_OBJECT); + closure = Z_OBJ_P(closure_zv); + ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure); CACHE_PTR(opline->extended_value, closure); } } else { From 7ec8ba80cb4630da52d799bc4721aea22881c303 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 30 Sep 2025 23:19:53 +0200 Subject: [PATCH 10/10] Remove now unneeded callable_convert_dtor() --- Zend/zend_execute_API.c | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 99335cd17aeef..674da0258ae41 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -125,14 +125,6 @@ static int clean_non_persistent_class_full(zval *zv) /* {{{ */ } /* }}} */ -static void callable_convert_dtor(zval *object_ptr) -{ - zend_object *object = Z_PTR_P(object_ptr); - if (zend_gc_delref(&object->gc) == 0) { - zend_objects_store_del(object); - } -} - void init_executor(void) /* {{{ */ { zend_init_fpu(); @@ -211,7 +203,7 @@ void init_executor(void) /* {{{ */ zend_fiber_init(); zend_weakrefs_init(); - zend_hash_init(&EG(callable_convert_cache), 8, NULL, callable_convert_dtor, 0); + zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0); EG(active) = 1; }