Commit f47cd216e95 for php.net
commit f47cd216e9537301471d6af770998fe5fe5c784c
Author: Ilija Tovilo <ilija.tovilo@me.com>
Date: Mon Sep 22 23:27:07 2025 +0200
Add stateless closure cache
RFC: https://wiki.php.net/rfc/closure-optimizations#stateless_closure_caching
Extracted from GH-19941. Sadly, @DanielEScherzer discovered an edge-case I did
not consider:
class Foo {
public function instanceCall() {
return $this;
}
public function test($c) {
return array_map($c, [1]);
}
}
$foo = new Foo();
var_dump($foo->test('Foo::instanceCall'));
Here, the internal function array_map() can perform instance calls through a
previous stack frame with just a named closure. Same with ['Foo',
'instanceCall']. That's quite esoteric, but this behavior should first be
deprecated and removed before assuming it isn't being used.
For the time being, merge only the latter half of the RFC. Code with all
relevant functions properly annotated as static will get the full performance
benefit.
Closes GH-23203
diff --git a/NEWS b/NEWS
index e5aa1098ffe..f60af7c1b76 100644
--- a/NEWS
+++ b/NEWS
@@ -19,6 +19,7 @@ PHP NEWS
handler). (David Carlier)
. Passing a 3rd argument to define() is now deprecated. (Girgias)
. Naming a function readonly is now deprecated. (Girgias)
+ . Added stateless closure cache. (ilutov)
- BZ2:
. Passing an object for the Bzip2 {de}compression stream filter is now
diff --git a/UPGRADING b/UPGRADING
index 0d32f455f32..779c95ae953 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -792,6 +792,8 @@ PHP 8.6 UPGRADE NOTES
. The TAILCALL VM is now enabled on Windows when compiling with Clang >= 19
x86_64.
. The performance of ZTS builds has been improved.
+ . Added stateless closure cache.
+ RFC: https://wiki.php.net/rfc/closure-optimizations#stateless_closure_caching
- DOM:
. Made splitText() faster and consume less memory.
diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c
index e43da5892b9..8277092d76b 100644
--- a/Zend/Optimizer/compact_literals.c
+++ b/Zend/Optimizer/compact_literals.c
@@ -756,6 +756,7 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
}
break;
case ZEND_CALLABLE_CONVERT:
+ case ZEND_DECLARE_LAMBDA_FUNCTION:
if (opline->extended_value != (uint32_t)-1) {
opline->extended_value = cache_size;
cache_size += sizeof(void *);
diff --git a/Zend/tests/partial_application/pipe_optimization_004.phpt b/Zend/tests/partial_application/pipe_optimization_004.phpt
index 194f08a9c14..2d70493f9d6 100644
--- a/Zend/tests/partial_application/pipe_optimization_004.phpt
+++ b/Zend/tests/partial_application/pipe_optimization_004.phpt
@@ -70,7 +70,7 @@ function foo($a, $b) {
; (lines=3, args=0, vars=0, tmps=%d)
; (after optimizer)
; %s:1-10
-0000 T0 = DECLARE_LAMBDA_FUNCTION 0
+0000 T0 = DECLARE_LAMBDA_FUNCTION %d 0
0001 FREE T0
0002 RETURN int(1)
diff --git a/Zend/tests/partial_application/pipe_optimization_007.phpt b/Zend/tests/partial_application/pipe_optimization_007.phpt
index be8d773c44c..03c98e26423 100644
--- a/Zend/tests/partial_application/pipe_optimization_007.phpt
+++ b/Zend/tests/partial_application/pipe_optimization_007.phpt
@@ -70,7 +70,7 @@ function foo($a, $b) {
; (lines=3, args=0, vars=0, tmps=%d)
; (after optimizer)
; %s:1-10
-0000 T0 = DECLARE_LAMBDA_FUNCTION 0
+0000 T0 = DECLARE_LAMBDA_FUNCTION %d 0
0001 FREE T0
0002 RETURN int(1)
diff --git a/Zend/tests/partial_application/pipe_optimization_008.phpt b/Zend/tests/partial_application/pipe_optimization_008.phpt
index 96f8d88815c..baa0f3d8d4c 100644
--- a/Zend/tests/partial_application/pipe_optimization_008.phpt
+++ b/Zend/tests/partial_application/pipe_optimization_008.phpt
@@ -66,7 +66,7 @@ function foo($a, $b) {
; (lines=4, args=0, vars=1, tmps=%d)
; (after optimizer)
; %s:1-10
-0000 T1 = DECLARE_LAMBDA_FUNCTION 0
+0000 T1 = DECLARE_LAMBDA_FUNCTION %d 0
0001 BIND_LEXICAL T1 CV0($a)
0002 FREE T1
0003 RETURN int(1)
diff --git a/Zend/tests/partial_application/pipe_optimization_013.phpt b/Zend/tests/partial_application/pipe_optimization_013.phpt
index 6c2be6728e7..e4fb797657d 100644
--- a/Zend/tests/partial_application/pipe_optimization_013.phpt
+++ b/Zend/tests/partial_application/pipe_optimization_013.phpt
@@ -54,7 +54,7 @@ function foo($a, $b) {
; (lines=4, args=0, vars=1, tmps=%d)
; (after optimizer)
; %s:1-9
-0000 T1 = DECLARE_LAMBDA_FUNCTION 0
+0000 T1 = DECLARE_LAMBDA_FUNCTION %d 0
0001 BIND_LEXICAL T1 CV0($b)
0002 FREE T1
0003 RETURN int(1)
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index cac60319a0a..6fd4df02352 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -8941,6 +8941,7 @@ static zend_string *zend_begin_func_decl(znode *result, zend_op_array *op_array,
if (op_array->fn_flags & ZEND_ACC_CLOSURE) {
opline = zend_emit_op_tmp(result, ZEND_DECLARE_LAMBDA_FUNCTION, NULL, NULL);
opline->op2.num = func_ref;
+ opline->extended_value = (uint32_t)-1;
} else {
opline = get_next_op();
opline->opcode = ZEND_DECLARE_FUNCTION;
@@ -9130,6 +9131,23 @@ static zend_op_array *zend_compile_func_decl_ex(
zend_compile_stmt(stmt_ast);
+ if (decl->kind == ZEND_AST_CLOSURE || decl->kind == ZEND_AST_ARROW_FUNC) {
+ zend_op_array *declaring_op_array = orig_oparray_context.op_array;
+
+ if ((op_array->fn_flags & ZEND_ACC_STATIC)
+ && !op_array->static_variables
+ /* Don't cache closures in main, as those would leak without a proper
+ * cleanup mechanism. */
+ && declaring_op_array->function_name
+ && declaring_op_array->last) {
+ zend_op *declare_lambda_op = &declaring_op_array->opcodes[declaring_op_array->last - 1];
+ if (declare_lambda_op->opcode == ZEND_DECLARE_LAMBDA_FUNCTION) {
+ declare_lambda_op->extended_value = declaring_op_array->cache_size;
+ declaring_op_array->cache_size += sizeof(void *);
+ }
+ }
+ }
+
if (is_method) {
CG(zend_lineno) = decl->start_lineno;
zend_check_magic_method_implementation(
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index 83ba5e7490c..cc99a75a6ab 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -203,6 +203,7 @@ void init_executor(void) /* {{{ */
zend_hash_init(&EG(callable_convert_cache), 8, NULL, ZVAL_PTR_DTOR, 0);
zend_hash_init(&EG(partial_function_application_cache), 8, NULL, zend_partial_op_array_dtor, 0);
+ zend_stack_init(&EG(lambda_cache), sizeof(zend_object *));
EG(active) = 1;
}
@@ -268,6 +269,14 @@ void shutdown_destructors(void) /* {{{ */
}
/* }}} */
+static void lambda_dtor(zend_object **closure_ptr)
+{
+ zend_object *closure = *closure_ptr;
+ if (GC_DELREF(closure) == 0) {
+ zend_objects_store_del(closure);
+ }
+}
+
/* Free values held by the executor. */
ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
{
@@ -421,6 +430,7 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
zend_hash_clean(&EG(callable_convert_cache));
zend_hash_clean(&EG(partial_function_application_cache));
+ zend_stack_clean(&EG(lambda_cache), (void (*)(void *)) lambda_dtor, 1);
#if ZEND_DEBUG
if (!CG(unclean_shutdown)) {
diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h
index 83360a2c96d..4d5e300e285 100644
--- a/Zend/zend_globals.h
+++ b/Zend/zend_globals.h
@@ -326,6 +326,7 @@ struct _zend_executor_globals {
HashTable callable_convert_cache;
HashTable partial_function_application_cache;
+ zend_stack lambda_cache;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
};
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index d89d460d9d9..cf8072646ee 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -8409,13 +8409,21 @@ ZEND_VM_HANDLER(210, ZEND_DECLARE_ATTRIBUTED_CONST, CONST, CONST)
ZEND_VM_NEXT_OPCODE_EX(1, 2);
}
-ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, CONST, NUM)
+ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, UNUSED, NUM, NUM|CACHE_SLOT)
{
USE_OPLINE
zend_function *func;
zval *object;
zend_class_entry *called_scope;
+ if (opline->extended_value != (uint32_t)-1) {
+ zend_object *closure = CACHED_PTR(opline->extended_value);
+ if (closure) {
+ ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
+ ZEND_VM_NEXT_OPCODE();
+ }
+ }
+
func = (zend_function *) EX(func)->op_array.dynamic_func_defs[opline->op2.num];
if (Z_TYPE(EX(This)) == IS_OBJECT) {
called_scope = Z_OBJCE(EX(This));
@@ -8432,7 +8440,12 @@ ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, CONST, NUM)
SAVE_OPLINE();
zend_create_closure(EX_VAR(opline->result.var), func,
EX(func)->op_array.scope, called_scope, object);
-
+ if (opline->extended_value != (uint32_t)-1) {
+ zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var));
+ GC_ADDREF(closure);
+ CACHE_PTR(opline->extended_value, closure);
+ zend_stack_push(&EG(lambda_cache), &closure);
+ }
ZEND_VM_NEXT_OPCODE();
}
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 4c830f203bf..58cd0ddf916 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/Zend/zend_vm_handlers.h b/Zend/zend_vm_handlers.h
index 503a6d25634..4586fd86102 100644
Binary files a/Zend/zend_vm_handlers.h and b/Zend/zend_vm_handlers.h differ
diff --git a/Zend/zend_vm_opcodes.c b/Zend/zend_vm_opcodes.c
index f9b30edb5e9..cc14862ebed 100644
Binary files a/Zend/zend_vm_opcodes.c and b/Zend/zend_vm_opcodes.c differ
diff --git a/ext/opcache/tests/array_map_foreach_optimization_008.phpt b/ext/opcache/tests/array_map_foreach_optimization_008.phpt
index 36c80b460e8..e90980922ca 100644
--- a/ext/opcache/tests/array_map_foreach_optimization_008.phpt
+++ b/ext/opcache/tests/array_map_foreach_optimization_008.phpt
@@ -56,7 +56,7 @@ function plusn($x, $n) {
; (lines=4, args=0, vars=1, tmps=%d)
; (after optimizer)
; %s:1-9
-0000 T1 = DECLARE_LAMBDA_FUNCTION 0
+0000 T1 = DECLARE_LAMBDA_FUNCTION %d 0
0001 BIND_LEXICAL T1 CV0($n)
0002 FREE T1
0003 RETURN int(1)
diff --git a/ext/opcache/tests/gh19867.phpt b/ext/opcache/tests/gh19867.phpt
index 486a366722d..51b2aaeaf45 100644
--- a/ext/opcache/tests/gh19867.phpt
+++ b/ext/opcache/tests/gh19867.phpt
@@ -15,7 +15,7 @@
; (lines=%d, args=0, vars=%d, tmps=%d)
; (after optimizer)
; %s
-0000 T0 = DECLARE_LAMBDA_FUNCTION 0
+0000 T0 = DECLARE_LAMBDA_FUNCTION %d 0
0001 FREE T0
0002 RETURN int(1)
@@ -23,7 +23,7 @@
; (lines=%d, args=0, vars=%d, tmps=%d)
; (after optimizer)
; %s
-0000 T0 = DECLARE_LAMBDA_FUNCTION 0
+0000 T0 = DECLARE_LAMBDA_FUNCTION %d 0
0001 RETURN T0
{closure:%s:%d}: