Commit eedda2a03c6 for php.net
commit eedda2a03c681f12e7aaef87fc747191bd047a67
Author: Alexandru Pătrănescu <drealecs@gmail.com>
Date: Fri May 8 15:42:23 2026 +0200
Add consumed_args callback optimization and apply to array_reduce() for carry to improve performance (GH-21340)
* Add consumed_args support for fcall arguments
* Use consumed_args for array_reduce and some other callback-using functions
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index a4297eec587..b17922f84c3 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -103,6 +103,10 @@ PHP 8.6 INTERNALS UPGRADE NOTES
. The deprecated Z_IMMUTABLE(), Z_IMMUTABLE_P(), Z_OPT_IMMUTABLE(), and
Z_OPT_IMMUTABLE_P() macros have been removed. Check for
IS_ARRAY && !REFCOUNTED directly.
+ . Added zend_fcall_info.consumed_args together with
+ zend_fci_consumed_arg(), which allows moving a selected callback argument
+ instead of copying it in zend_call_function(). Currently only a single
+ consumed argument is supported.
========================
2. Build system changes
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index ccb770a9963..004b4fd81e9 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -4243,6 +4243,7 @@ ZEND_API zend_result zend_fcall_info_init(const zval *callable, uint32_t check_f
fci->param_count = 0;
fci->params = NULL;
fci->named_params = NULL;
+ fci->consumed_args = 0;
return SUCCESS;
}
diff --git a/Zend/zend_API.h b/Zend/zend_API.h
index 01a9202be1c..2487c8b632f 100644
--- a/Zend/zend_API.h
+++ b/Zend/zend_API.h
@@ -48,6 +48,7 @@ typedef struct _zend_fcall_info {
zval *params;
zend_object *object;
uint32_t param_count;
+ uint32_t consumed_args;
/* This hashtable can also contain positional arguments (with integer keys),
* which will be appended to the normal params[]. This makes it easier to
* integrate APIs like call_user_func_array(). The usual restriction that
@@ -341,6 +342,13 @@ typedef struct _zend_fcall_info_cache {
#define ZEND_FCI_INITIALIZED(fci) ((fci).size != 0)
#define ZEND_FCC_INITIALIZED(fcc) ((fcc).function_handler != NULL)
+static zend_always_inline uint32_t zend_fci_consumed_arg(uint32_t arg_index) {
+ return arg_index < 32 ? (UINT32_C(1) << arg_index) : UINT32_C(0);
+}
+static zend_always_inline bool zend_fci_is_consumed_arg(uint32_t consumed_args, uint32_t arg_index) {
+ return arg_index < 32 && (consumed_args & (UINT32_C(1) << arg_index));
+}
+
ZEND_API int zend_next_free_module(void);
BEGIN_EXTERN_C()
diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c
index b46085c80af..9763dd34613 100644
--- a/Zend/zend_closures.c
+++ b/Zend/zend_closures.c
@@ -154,6 +154,7 @@ ZEND_METHOD(Closure, call)
fci_cache.object = fci.object = newobj;
fci.size = sizeof(fci);
+ fci.consumed_args = 0;
ZVAL_OBJ(&fci.function_name, &closure->std);
ZVAL_UNDEF(&closure_result);
fci.retval = &closure_result;
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index 0022eb4a1df..71e0c56a51c 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -795,6 +795,7 @@ zend_result _call_user_function_impl(zval *object, zval *function_name, zval *re
fci.param_count = param_count;
fci.params = params;
fci.named_params = named_params;
+ fci.consumed_args = 0;
return zend_call_function(&fci, NULL);
}
@@ -862,6 +863,9 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
call = zend_vm_stack_push_call_frame(call_info,
func, fci->param_count, object_or_called_scope);
+ uint32_t consumed_args = fci->param_count ? fci->consumed_args : 0;
+
+ ZEND_ASSERT((consumed_args & (consumed_args - 1)) == 0);
for (uint32_t i = 0; i < fci->param_count; i++) {
zval *param = ZEND_CALL_ARG(call, i+1);
@@ -903,7 +907,15 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
}
if (EXPECTED(!must_wrap)) {
- ZVAL_COPY(param, arg);
+ if (EXPECTED(consumed_args == 0)
+ || !zend_fci_is_consumed_arg(consumed_args, i)
+ || Z_ISREF_P(arg)
+ || arg != &fci->params[i]) {
+ ZVAL_COPY(param, arg);
+ } else {
+ ZVAL_COPY_VALUE(param, arg);
+ ZVAL_UNDEF(arg);
+ }
} else {
Z_TRY_ADDREF_P(arg);
ZVAL_NEW_REF(param, arg);
@@ -1092,6 +1104,7 @@ ZEND_API void zend_call_known_function(
fci.param_count = param_count;
fci.params = params;
fci.named_params = named_params;
+ fci.consumed_args = 0;
ZVAL_UNDEF(&fci.function_name); /* Unused */
fcic.function_handler = fn;
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index b1a2f808ca6..90395dc15f2 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -406,6 +406,7 @@ static zend_result php_dom_xpath_callback_dispatch(php_dom_xpath_callbacks *xpat
fci.param_count = param_count;
fci.params = params;
fci.named_params = NULL;
+ fci.consumed_args = 0;
ZVAL_STRINGL(&fci.function_name, function_name, function_name_length);
zend_call_function(&fci, NULL);
diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c
index cc0c4c29744..779b41fcad3 100644
--- a/ext/ffi/ffi.c
+++ b/ext/ffi/ffi.c
@@ -948,6 +948,7 @@ static void zend_ffi_callback_trampoline(ffi_cif* cif, void* ret, void** args, v
fci.object = NULL;
fci.param_count = callback_data->arg_count;
fci.named_params = NULL;
+ fci.consumed_args = 0;
if (callback_data->type->func.args) {
int n = 0;
diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c
index 29c2551f38f..b9f8e1211ff 100644
--- a/ext/pcre/php_pcre.c
+++ b/ext/pcre/php_pcre.c
@@ -1559,6 +1559,7 @@ static zend_string *preg_do_repl_func(zend_fcall_info *fci, zend_fcall_info_cach
fci->retval = &retval;
fci->param_count = 1;
fci->params = &arg;
+ fci->consumed_args = zend_fci_consumed_arg(0);
zend_call_function(fci, fcc);
zval_ptr_dtor(&arg);
if (EXPECTED(Z_TYPE(retval) == IS_STRING)) {
diff --git a/ext/pcre/tests/preg_replace_callback_matches_refcount.phpt b/ext/pcre/tests/preg_replace_callback_matches_refcount.phpt
new file mode 100644
index 00000000000..0ae802654c9
--- /dev/null
+++ b/ext/pcre/tests/preg_replace_callback_matches_refcount.phpt
@@ -0,0 +1,37 @@
+--TEST--
+preg_replace_callback(): capture match array refcount stays low during callback
+--FILE--
+<?php
+var_dump(preg_replace_callback('/(.)(.)(.)/', static function ($matches) {
+ debug_zval_dump($matches);
+ return '';
+}, 'abc'));
+
+var_dump(preg_replace_callback_array(['/(.)(.)(.)/' => static function ($matches) {
+ debug_zval_dump($matches);
+ return '';
+}], 'abc'));
+?>
+--EXPECTF--
+array(4) packed refcount(2){
+ [0]=>
+ string(3) "abc"%s
+ [1]=>
+ string(1) "a" interned
+ [2]=>
+ string(1) "b" interned
+ [3]=>
+ string(1) "c" interned
+}
+string(0) ""
+array(4) packed refcount(2){
+ [0]=>
+ string(3) "abc"%s
+ [1]=>
+ string(1) "a" interned
+ [2]=>
+ string(1) "b" interned
+ [3]=>
+ string(1) "c" interned
+}
+string(0) ""
diff --git a/ext/standard/array.c b/ext/standard/array.c
index e175b7b8d2f..49a3bbd557b 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -6417,6 +6417,7 @@ PHP_FUNCTION(array_reduce)
fci.retval = return_value;
fci.param_count = 2;
fci.params = args;
+ fci.consumed_args = zend_fci_consumed_arg(0);
ZEND_HASH_FOREACH_VAL(htbl, operand) {
ZVAL_COPY_VALUE(&args[0], return_value);
diff --git a/ext/standard/tests/array/array_reduce_accumulator_refcount.phpt b/ext/standard/tests/array/array_reduce_accumulator_refcount.phpt
new file mode 100644
index 00000000000..f2a0639b345
--- /dev/null
+++ b/ext/standard/tests/array/array_reduce_accumulator_refcount.phpt
@@ -0,0 +1,35 @@
+--TEST--
+array_reduce(): accumulator refcount stays low during callback
+--FILE--
+<?php
+
+$result = array_reduce([1, 2, 3], function ($acc, $val) {
+ debug_zval_dump($acc);
+ $acc[] = $val;
+ return $acc;
+}, []);
+
+debug_zval_dump($result);
+
+?>
+--EXPECT--
+array(0) interned {
+}
+array(1) packed refcount(2){
+ [0]=>
+ int(1)
+}
+array(2) packed refcount(2){
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+}
+array(3) packed refcount(2){
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+}
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index b947ad3302a..48d28a8a4c3 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -545,6 +545,76 @@ static ZEND_FUNCTION(zend_call_method_if_exists)
}
}
+static ZEND_FUNCTION(zend_test_call_with_consumed_args)
+{
+ zend_fcall_info fci = empty_fcall_info;
+ zend_fcall_info_cache fcc = empty_fcall_info_cache;
+ zval *args;
+ zend_long consumed_args;
+ zval retval;
+ uint32_t actual_consumed_args = 0;
+ uint32_t i;
+ zend_result call_result;
+
+ ZEND_PARSE_PARAMETERS_START(3, 3)
+ Z_PARAM_FUNC(fci, fcc)
+ Z_PARAM_ARRAY(args)
+ Z_PARAM_LONG(consumed_args)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (UNEXPECTED(consumed_args < 0 || consumed_args > UINT32_MAX)) {
+ zend_argument_value_error(3, "must be between 0 and 4294967295");
+ RETURN_THROWS();
+ }
+
+ zend_fcall_info_args(&fci, args);
+
+ ZVAL_UNDEF(&retval);
+ fci.retval = &retval;
+ fci.consumed_args = (uint32_t) consumed_args;
+
+ call_result = zend_call_function(&fci, &fcc);
+
+ for (i = 0; i < fci.param_count && i < 32; i++) {
+ if (Z_ISUNDEF(fci.params[i])) {
+ actual_consumed_args |= (1u << i);
+ }
+ }
+
+ zend_fcall_info_args_clear(&fci, true);
+
+ if (call_result == FAILURE || EG(exception)) {
+ if (!Z_ISUNDEF(retval)) {
+ zval_ptr_dtor(&retval);
+ }
+ RETURN_THROWS();
+ }
+
+ array_init(return_value);
+ add_assoc_long(return_value, "consumed_args", actual_consumed_args);
+
+ if (Z_ISUNDEF(retval)) {
+ add_assoc_null(return_value, "retval");
+ } else {
+ add_assoc_zval(return_value, "retval", &retval);
+ }
+}
+
+static ZEND_FUNCTION(zend_test_refcount)
+{
+ zval *value;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ZVAL(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (!Z_REFCOUNTED_P(value)) {
+ RETURN_LONG(-1);
+ }
+
+ RETURN_LONG(Z_REFCOUNT_P(value));
+}
+
static ZEND_FUNCTION(zend_get_unit_enum)
{
ZEND_PARSE_PARAMETERS_NONE();
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index 653630ed73b..489d7d0a260 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -305,6 +305,10 @@ function zend_object_init_with_constructor(string $class, mixed ...$args): mixed
function zend_call_method_if_exists(object $obj, string $method, mixed ...$args): mixed {}
+ function zend_test_call_with_consumed_args(callable $cb, array $args, int $consumed_args): array {}
+
+ function zend_test_refcount(mixed $value): int {}
+
function zend_test_zend_ini_parse_quantity(string $str): int {}
function zend_test_zend_ini_parse_uquantity(string $str): int {}
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index adcae16cdf6..94f75cdb360 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h
index ba6aab90228..4a6babbe12b 100644
Binary files a/ext/zend_test/test_decl.h and b/ext/zend_test/test_decl.h differ
diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h
index b42d524d7a8..a4c1ae3f2c9 100644
Binary files a/ext/zend_test/test_legacy_arginfo.h and b/ext/zend_test/test_legacy_arginfo.h differ
diff --git a/ext/zend_test/tests/consumed_args_basic.phpt b/ext/zend_test/tests/consumed_args_basic.phpt
new file mode 100644
index 00000000000..711ac812457
--- /dev/null
+++ b/ext/zend_test/tests/consumed_args_basic.phpt
@@ -0,0 +1,43 @@
+--TEST--
+zend_test_call_with_consumed_args(): consume a non-reference arg
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$result = zend_test_call_with_consumed_args(
+ static function ($a, $b) {
+ debug_zval_dump($a);
+ return [$a, $b];
+ },
+ [range(1, 3), "x"],
+ 1,
+);
+
+var_dump($result["consumed_args"]);
+var_dump($result["retval"]);
+
+?>
+--EXPECTF--
+array(3) %srefcount(3){
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+}
+int(1)
+array(2) {
+ [0]=>
+ array(3) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+ }
+ [1]=>
+ string(1) "x"
+}
diff --git a/ext/zend_test/tests/consumed_args_ob_start_refcount.phpt b/ext/zend_test/tests/consumed_args_ob_start_refcount.phpt
new file mode 100644
index 00000000000..3d5088d284d
--- /dev/null
+++ b/ext/zend_test/tests/consumed_args_ob_start_refcount.phpt
@@ -0,0 +1,23 @@
+--TEST--
+ob_start(): consumed callback arg has low refcount
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$counts = [];
+ob_start(static function ($buffer) use (&$counts) {
+ $counts[] = zend_test_refcount($buffer);
+ return '';
+});
+echo 'abc';
+ob_end_flush();
+
+var_dump($counts);
+
+?>
+--EXPECT--
+array(1) {
+ [0]=>
+ int(2)
+}
diff --git a/ext/zend_test/tests/consumed_args_ref_arg.phpt b/ext/zend_test/tests/consumed_args_ref_arg.phpt
new file mode 100644
index 00000000000..a528860006b
--- /dev/null
+++ b/ext/zend_test/tests/consumed_args_ref_arg.phpt
@@ -0,0 +1,35 @@
+--TEST--
+zend_test_call_with_consumed_args(): do not consume reference args
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$value = [10];
+
+$result = zend_test_call_with_consumed_args(
+ static function ($a) {
+ $a[] = 20;
+ return $a;
+ },
+ [&$value],
+ 1,
+);
+
+var_dump($result["consumed_args"]);
+var_dump($result["retval"]);
+var_dump($value);
+
+?>
+--EXPECT--
+int(0)
+array(2) {
+ [0]=>
+ int(10)
+ [1]=>
+ int(20)
+}
+array(1) {
+ [0]=>
+ int(10)
+}
diff --git a/ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt b/ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt
new file mode 100644
index 00000000000..01d4025842a
--- /dev/null
+++ b/ext/zend_test/tests/consumed_args_ref_param_and_ref_arg.phpt
@@ -0,0 +1,37 @@
+--TEST--
+zend_test_call_with_consumed_args(): ref-required params with reference args are not consumed
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$value = [10];
+
+$result = zend_test_call_with_consumed_args(
+ static function (&$a) {
+ $a[] = 20;
+ return $a;
+ },
+ [&$value],
+ 1,
+);
+
+var_dump($result["consumed_args"]);
+var_dump($result["retval"]);
+var_dump($value);
+
+?>
+--EXPECT--
+int(0)
+array(2) {
+ [0]=>
+ int(10)
+ [1]=>
+ int(20)
+}
+array(2) {
+ [0]=>
+ int(10)
+ [1]=>
+ int(20)
+}
diff --git a/ext/zend_test/tests/consumed_args_ref_required.phpt b/ext/zend_test/tests/consumed_args_ref_required.phpt
new file mode 100644
index 00000000000..f261131a686
--- /dev/null
+++ b/ext/zend_test/tests/consumed_args_ref_required.phpt
@@ -0,0 +1,24 @@
+--TEST--
+zend_test_call_with_consumed_args(): ref-required params are not consumed
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$result = zend_test_call_with_consumed_args(
+ static function (&$a) {
+ $a = 42;
+ return $a;
+ },
+ [1],
+ 1,
+);
+
+var_dump($result["consumed_args"]);
+var_dump($result["retval"]);
+
+?>
+--EXPECTF--
+Warning: {closure:%s:%d}(): Argument #1 ($a) must be passed by reference, value given in %s on line %d
+int(0)
+int(42)
diff --git a/main/output.c b/main/output.c
index 72b42301a72..664adf7f168 100644
--- a/main/output.c
+++ b/main/output.c
@@ -970,6 +970,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl
handler->func.user->fci.param_count = 2;
handler->func.user->fci.params = ob_args;
handler->func.user->fci.retval = &retval;
+ handler->func.user->fci.consumed_args = zend_fci_consumed_arg(0);
if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) {
if (handler->flags & PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT) {