Commit a3576bddc52 for php.net
commit a3576bddc52310e8ed6c545e38104e884713ceef
Author: Tim Düsterhus <tim@tideways-gmbh.com>
Date: Mon Jan 19 10:18:24 2026 +0100
zend_compile: Optimize `array_map()` with callable convert callback into foreach (#20934)
* zend_compile: Optimize `array_map()` with callable convert callback into foreach
For:
<?php
function plus1($x) {
return $x + 1;
}
$array = array_fill(0, 100, 1);
$count = 0;
for ($i = 0; $i < 100_000; $i++) {
$count += count(array_map(plus1(...), $array));
}
var_dump($count);
This is ~1.1× faster:
Benchmark 1: /tmp/test/before -d opcache.enable_cli=1 /tmp/test/test6.php
Time (mean ± σ): 172.2 ms ± 0.5 ms [User: 167.8 ms, System: 4.2 ms]
Range (min … max): 171.6 ms … 173.1 ms 17 runs
Benchmark 2: /tmp/test/after -d opcache.enable_cli=1 /tmp/test/test6.php
Time (mean ± σ): 155.1 ms ± 1.3 ms [User: 150.6 ms, System: 4.2 ms]
Range (min … max): 154.2 ms … 159.3 ms 18 runs
Summary
/tmp/test/after -d opcache.enable_cli=1 /tmp/test/test6.php ran
1.11 ± 0.01 times faster than /tmp/test/before -d opcache.enable_cli=1 /tmp/test/test6.php
With JIT it becomes ~1.7× faster:
Benchmark 1: /tmp/test/before -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php
Time (mean ± σ): 166.9 ms ± 0.6 ms [User: 162.7 ms, System: 4.1 ms]
Range (min … max): 166.1 ms … 167.9 ms 17 runs
Benchmark 2: /tmp/test/after -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php
Time (mean ± σ): 94.5 ms ± 2.7 ms [User: 90.4 ms, System: 3.9 ms]
Range (min … max): 92.5 ms … 103.1 ms 31 runs
Summary
/tmp/test/after -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php ran
1.77 ± 0.05 times faster than /tmp/test/before -d opcache.enable_cli=1 -d opcache.jit=tracing /tmp/test/test6.php
* zend_compile: Skip `assert(...)` callbacks for array_map() optimization
* zend_compile: Remove `zend_eval_const_expr()` in array_map optimization
* zend_vm_def: Check simple types without loading the arginfo in ZEND_TYPE_ASSERT
* zend_vm_def: Handle references for ZEND_TYPE_ASSERT
* zend_compile: Fix handling of constant arrays for `array_map()`
* zend_compile: Fix leak of unused result in array_map() optimization
* zend_compile: Support static methods for `array_map()` optimization
* UPGRADING
diff --git a/UPGRADING b/UPGRADING
index 0b651e5895c..3d0a56756cd 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -151,6 +151,11 @@ PHP 8.6 UPGRADE NOTES
parsing the format string.
. Arguments are now passed more efficiently to known constructors (e.g. when
using new self()).
+ . array_map() using a first-class callable or partial function application
+ callback will be compiled into the equivalent foreach-loop, avoiding the
+ creation of intermediate Closures, the overhead of calling userland
+ callbacks from internal functions and providing for better insight for the
+ JIT.
- DOM:
. Made splitText() faster and consume less memory.
diff --git a/Zend/tests/functions/zend_call_function_deprecated_frame.phpt b/Zend/tests/functions/zend_call_function_deprecated_frame.phpt
index ce2943a876e..83d05144c0b 100644
--- a/Zend/tests/functions/zend_call_function_deprecated_frame.phpt
+++ b/Zend/tests/functions/zend_call_function_deprecated_frame.phpt
@@ -19,7 +19,6 @@ function foo(string $v) {
--EXPECTF--
Fatal error: Uncaught Exception: Function foo() is deprecated in %s:%d
Stack trace:
-#0 [internal function]: {closure:%s:%d}(16384, 'Function foo() ...', '%s', %d)
-#1 %s(%d): array_map(Object(Closure), Array)
-#2 {main}
+#0 %s(%d): {closure:%s:%d}(16384, 'Function foo() ...', '%s', %d)
+#1 {main}
thrown in %s on line %d
diff --git a/Zend/tests/gh14003.phpt b/Zend/tests/gh14003.phpt
index 92a6c5919ab..8d25cad863c 100644
--- a/Zend/tests/gh14003.phpt
+++ b/Zend/tests/gh14003.phpt
@@ -18,7 +18,6 @@ function foo(string $key): string {
--EXPECTF--
Fatal error: Uncaught Exception: Test in %s:%d
Stack trace:
-#0 [internal function]: foo('a')
-#1 %s(%d): array_map(Object(Closure), Array)
-#2 {main}
+#0 %s(%d): foo('a')
+#1 {main}
thrown in %s on line %d
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 203201bcc12..916d8eebd89 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -5027,7 +5027,115 @@ static zend_result zend_compile_func_clone(znode *result, const zend_ast_list *a
return SUCCESS;
}
-static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, uint32_t type) /* {{{ */
+static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *args, zend_string *lcname, uint32_t lineno) /* {{{ */
+{
+ /* Bail out if we do not have exactly two parameters. */
+ if (args->children != 2) {
+ return FAILURE;
+ }
+
+ zend_ast *callback = args->child[0];
+
+ /* Bail out if the callback is not a FCC/PFA. */
+ zend_ast *args_ast;
+ switch (callback->kind) {
+ case ZEND_AST_CALL:
+ case ZEND_AST_STATIC_CALL:
+ args_ast = zend_ast_call_get_args(callback);
+ if (args_ast->kind != ZEND_AST_CALLABLE_CONVERT) {
+ return FAILURE;
+ }
+
+ break;
+ default:
+ return FAILURE;
+ }
+
+ /* Bail out if the callback is assert() due to the AST stringification logic
+ * breaking for the generated call.
+ */
+ if (callback->kind == ZEND_AST_CALL && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) {
+ return FAILURE;
+ }
+
+ znode value;
+ value.op_type = IS_TMP_VAR;
+ value.u.op.var = get_temporary_variable();
+
+ zend_ast_list *callback_args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);
+ zend_ast *call_args = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
+ for (uint32_t i = 0; i < callback_args->children; i++) {
+ zend_ast *child = callback_args->child[i];
+ if (child->kind == ZEND_AST_PLACEHOLDER_ARG) {
+ call_args = zend_ast_list_add(call_args, zend_ast_create_znode(&value));
+ } else {
+ ZEND_ASSERT(0 && "not implemented");
+ call_args = zend_ast_list_add(call_args, child);
+ }
+ }
+
+ zend_op *opline;
+
+ znode array;
+ zend_compile_expr(&array, args->child[1]);
+ /* array is an argument to both ZEND_TYPE_ASSERT and to ZEND_FE_RESET_R. */
+ if (array.op_type == IS_CONST) {
+ Z_TRY_ADDREF(array.u.constant);
+ }
+
+ /* Verify that the input array actually is an array. */
+ znode name;
+ name.op_type = IS_CONST;
+ ZVAL_STR_COPY(&name.u.constant, lcname);
+ opline = zend_emit_op(NULL, ZEND_TYPE_ASSERT, &name, &array);
+ opline->lineno = lineno;
+ opline->extended_value = (2 << 16) | IS_ARRAY;
+ const zval *fbc_zv = zend_hash_find(CG(function_table), lcname);
+ const Bucket *fbc_bucket = (const Bucket*)((uintptr_t)fbc_zv - XtOffsetOf(Bucket, val));
+ Z_EXTRA_P(CT_CONSTANT(opline->op1)) = fbc_bucket - CG(function_table)->arData;
+
+ /* Initialize the result array. */
+ zend_emit_op_tmp(result, ZEND_INIT_ARRAY, NULL, NULL);
+
+ /* foreach loop starts here. */
+ znode key;
+
+ uint32_t opnum_reset = get_next_op_number();
+ znode reset_node;
+ zend_emit_op(&reset_node, ZEND_FE_RESET_R, &array, NULL);
+ zend_begin_loop(ZEND_FE_FREE, &reset_node, false);
+ uint32_t opnum_fetch = get_next_op_number();
+ zend_emit_op_tmp(&key, ZEND_FE_FETCH_R, &reset_node, &value);
+
+ /* loop body */
+ znode call_result;
+ switch (callback->kind) {
+ case ZEND_AST_CALL:
+ zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args));
+ break;
+ case ZEND_AST_STATIC_CALL:
+ zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args));
+ break;
+ }
+ opline = zend_emit_op(NULL, ZEND_ADD_ARRAY_ELEMENT, &call_result, &key);
+ SET_NODE(opline->result, result);
+ /* end loop body */
+
+ zend_emit_jump(opnum_fetch);
+
+ uint32_t opnum_loop_end = get_next_op_number();
+ opline = &CG(active_op_array)->opcodes[opnum_reset];
+ opline->op2.opline_num = opnum_loop_end;
+ opline = &CG(active_op_array)->opcodes[opnum_fetch];
+ opline->extended_value = opnum_loop_end;
+
+ zend_end_loop(opnum_fetch, &reset_node);
+ zend_emit_op(NULL, ZEND_FE_FREE, &reset_node, NULL);
+
+ return SUCCESS;
+}
+
+static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, uint32_t type, uint32_t lineno) /* {{{ */
{
if (zend_string_equals_literal(lcname, "strlen")) {
return zend_compile_func_strlen(result, args);
@@ -5099,12 +5207,14 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *
return zend_compile_func_printf(result, args);
} else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) {
return zend_compile_func_clone(result, args);
+ } else if (zend_string_equals_literal(lcname, "array_map")) {
+ return zend_compile_func_array_map(result, args, lcname, lineno);
} else {
return FAILURE;
}
}
-static zend_result zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, const zend_function *fbc, uint32_t type) /* {{{ */
+static zend_result zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, const zend_function *fbc, uint32_t type, uint32_t lineno) /* {{{ */
{
if (CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS) {
return FAILURE;
@@ -5120,7 +5230,7 @@ static zend_result zend_try_compile_special_func(znode *result, zend_string *lcn
return FAILURE;
}
- if (zend_try_compile_special_func_ex(result, lcname, args, type) == SUCCESS) {
+ if (zend_try_compile_special_func_ex(result, lcname, args, type, lineno) == SUCCESS) {
return SUCCESS;
}
@@ -5263,7 +5373,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type)
if (!is_callable_convert &&
zend_try_compile_special_func(result, lcname,
- zend_ast_get_list(args_ast), fbc, type) == SUCCESS
+ zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS
) {
zend_string_release_ex(lcname, 0);
zval_ptr_dtor(&name_node.u.constant);
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 9840bf28040..2d059159a23 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -8846,6 +8846,38 @@ ZEND_VM_C_LABEL(type_check_resource):
}
}
+ZEND_VM_HOT_HANDLER(211, ZEND_TYPE_ASSERT, CONST, ANY, NUM)
+{
+ USE_OPLINE
+ SAVE_OPLINE();
+
+ zval *value = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
+
+ uint8_t actual_type = Z_TYPE_P(value);
+ uint8_t expected_type = opline->extended_value & 0xff;
+ /* Simple types can be checked directly. */
+ if (UNEXPECTED(actual_type != expected_type)) {
+ zend_function *fbc;
+ {
+ zval *fname = (zval*)RT_CONSTANT(opline, opline->op1);
+ ZEND_ASSERT(Z_EXTRA_P(fname) != 0);
+ fbc = Z_FUNC(EG(function_table)->arData[Z_EXTRA_P(fname)].val);
+ ZEND_ASSERT(fbc->type != ZEND_USER_FUNCTION);
+ }
+ uint16_t argno = opline->extended_value >> 16;
+ zend_arg_info *arginfo = &fbc->common.arg_info[argno - 1];
+
+ if (!zend_check_type(&arginfo->type, value, /* is_return_type */ false, /* is_internal */ true)) {
+ const char *param_name = get_function_arg_name(fbc, argno);
+ zend_string *expected = zend_type_to_string(arginfo->type);
+ zend_type_error("%s(): Argument #%d%s%s%s must be of type %s, %s given", ZSTR_VAL(fbc->common.function_name), argno, param_name ? " ($" : "", param_name ? param_name : "", param_name ? ")" : "", ZSTR_VAL(expected), zend_zval_value_name(value));
+ zend_string_release(expected);
+ }
+ }
+
+ ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
+}
+
ZEND_VM_HOT_HANDLER(122, ZEND_DEFINED, CONST, ANY, CACHE_SLOT)
{
USE_OPLINE
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 7aa0296aa42..38b869c4a4c 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 33d95114155..d52de1226df 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 1c7a31b3861..cb70e8ccd86 100644
Binary files a/Zend/zend_vm_opcodes.c and b/Zend/zend_vm_opcodes.c differ
diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h
index dae282705d5..7aae4d0e55f 100644
Binary files a/Zend/zend_vm_opcodes.h and b/Zend/zend_vm_opcodes.h differ
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_001.phpt b/ext/standard/tests/array/array_map_foreach_optimization_001.phpt
new file mode 100644
index 00000000000..49031942d20
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_001.phpt
@@ -0,0 +1,82 @@
+--TEST--
+array_map(): foreach optimization
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plus1($x) {
+ return $x + 1;
+}
+
+$array = range(1, 10);
+
+$foo = array_map(plus1(...), $array);
+
+var_dump($foo);
+
+?>
+--EXPECTF--
+$_main:
+ ; (lines=%d, args=0, vars=%d, tmps=%d)
+ ; (after optimizer)
+ ; %s
+0000 INIT_FCALL 2 %d string("range")
+0001 SEND_VAL int(1) 1
+0002 SEND_VAL int(10) 2
+0003 V2 = DO_ICALL
+0004 ASSIGN CV0($array) V2
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T2 = INIT_ARRAY 0 (packed) NEXT
+0007 V3 = FE_RESET_R CV0($array) 0014
+0008 T5 = FE_FETCH_R V3 T4 0014
+0009 INIT_FCALL 1 %d string("plus1")
+0010 SEND_VAL T4 1
+0011 V4 = DO_UCALL
+0012 T2 = ADD_ARRAY_ELEMENT V4 T5
+0013 JMP 0008
+0014 FE_FREE V3
+0015 ASSIGN CV1($foo) T2
+0016 INIT_FCALL 1 %d string("var_dump")
+0017 SEND_VAR CV1($foo) 1
+0018 DO_ICALL
+0019 RETURN int(1)
+LIVE RANGES:
+ 2: 0007 - 0015 (tmp/var)
+ 3: 0008 - 0014 (loop)
+ 4: 0009 - 0010 (tmp/var)
+ 5: 0009 - 0012 (tmp/var)
+
+plus1:
+ ; (lines=3, args=1, vars=1, tmps=1)
+ ; (after optimizer)
+ ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+array(10) {
+ [0]=>
+ int(2)
+ [1]=>
+ int(3)
+ [2]=>
+ int(4)
+ [3]=>
+ int(5)
+ [4]=>
+ int(6)
+ [5]=>
+ int(7)
+ [6]=>
+ int(8)
+ [7]=>
+ int(9)
+ [8]=>
+ int(10)
+ [9]=>
+ int(11)
+}
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_002.phpt b/ext/standard/tests/array/array_map_foreach_optimization_002.phpt
new file mode 100644
index 00000000000..79b7df08b61
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_002.phpt
@@ -0,0 +1,73 @@
+--TEST--
+array_map(): foreach optimization - Error
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plus1($x) {
+ return $x + 1;
+}
+
+$array = \range(1, 10);
+
+$foo = array_map(
+ plus1(...),
+ new \ArrayIterator($array)
+);
+
+\var_dump($foo);
+
+?>
+--EXPECTF--
+$_main:
+ ; (lines=%d, args=0, vars=%d, tmps=%d)
+ ; (after optimizer)
+ ; %s
+0000 INIT_FCALL 2 %d string("range")
+0001 SEND_VAL int(1) 1
+0002 SEND_VAL int(10) 2
+0003 V2 = DO_ICALL
+0004 ASSIGN CV0($array) V2
+0005 V5 = NEW 1 string("ArrayIterator")
+0006 SEND_VAR%S CV0($array) 1
+0007 DO_FCALL
+0008 TYPE_ASSERT 131079 string("array_map") V5
+0009 T2 = INIT_ARRAY 0 (packed) NEXT
+0010 V3 = FE_RESET_R V5 0017
+0011 T5 = FE_FETCH_R V3 T4 0017
+0012 INIT_FCALL 1 %d string("plus1")
+0013 SEND_VAL T4 1
+0014 V4 = DO_UCALL
+0015 T2 = ADD_ARRAY_ELEMENT V4 T5
+0016 JMP 0011
+0017 FE_FREE V3
+0018 ASSIGN CV1($foo) T2
+0019 INIT_FCALL 1 %d string("var_dump")
+0020 SEND_VAR CV1($foo) 1
+0021 DO_ICALL
+0022 RETURN int(1)
+LIVE RANGES:
+ 5: 0006 - 0008 (new)
+ 5: 0008 - 0010 (tmp/var)
+ 2: 0010 - 0018 (tmp/var)
+ 3: 0011 - 0017 (loop)
+ 4: 0012 - 0013 (tmp/var)
+ 5: 0012 - 0015 (tmp/var)
+
+plus1:
+ ; (lines=3, args=1, vars=1, tmps=1)
+ ; (after optimizer)
+ ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+
+Fatal error: Uncaught TypeError: array_map(): Argument #2 ($array) must be of type array, ArrayIterator given in %s:9
+Stack trace:
+#0 {main}
+ thrown in %s on line 9
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_003.phpt b/ext/standard/tests/array/array_map_foreach_optimization_003.phpt
new file mode 100644
index 00000000000..7a287f54f78
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_003.phpt
@@ -0,0 +1,73 @@
+--TEST--
+array_map(): foreach optimization - const array
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function plus1($x) {
+ return $x + 1;
+}
+
+$foo = array_map(plus1(...), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
+
+var_dump($foo);
+
+?>
+--EXPECTF--
+$_main:
+ ; (lines=%d, args=0, vars=%d, tmps=%d)
+ ; (after optimizer)
+ ; %s
+0000 TYPE_ASSERT 131079 string("array_map") array(...)
+0001 T1 = INIT_ARRAY 0 (packed) NEXT
+0002 V2 = FE_RESET_R array(...) 0009
+0003 T4 = FE_FETCH_R V2 T3 0009
+0004 INIT_FCALL 1 %d string("plus1")
+0005 SEND_VAL T3 1
+0006 V3 = DO_UCALL
+0007 T1 = ADD_ARRAY_ELEMENT V3 T4
+0008 JMP 0003
+0009 FE_FREE V2
+0010 ASSIGN CV0($foo) T1
+0011 INIT_FCALL 1 %d string("var_dump")
+0012 SEND_VAR CV0($foo) 1
+0013 DO_ICALL
+0014 RETURN int(1)
+LIVE RANGES:
+ 1: 0002 - 0010 (tmp/var)
+ 2: 0003 - 0009 (loop)
+
+plus1:
+ ; (lines=3, args=1, vars=1, tmps=1)
+ ; (after optimizer)
+ ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+array(10) {
+ [0]=>
+ int(2)
+ [1]=>
+ int(3)
+ [2]=>
+ int(4)
+ [3]=>
+ int(5)
+ [4]=>
+ int(6)
+ [5]=>
+ int(7)
+ [6]=>
+ int(8)
+ [7]=>
+ int(9)
+ [8]=>
+ int(10)
+ [9]=>
+ int(11)
+}
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_004.phpt b/ext/standard/tests/array/array_map_foreach_optimization_004.phpt
new file mode 100644
index 00000000000..b97ed1e31d3
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_004.phpt
@@ -0,0 +1,57 @@
+--TEST--
+array_map(): foreach optimization - unused refcounted result
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+function stdClass() {
+ return new stdClass();
+}
+
+$array = range(1, 10);
+
+array_map(stdClass(...), $array);
+
+?>
+--EXPECTF--
+$_main:
+ ; (lines=%d, args=0, vars=%d, tmps=%d)
+ ; (after optimizer)
+ ; %s
+0000 INIT_FCALL 2 %d string("range")
+0001 SEND_VAL int(1) 1
+0002 SEND_VAL int(10) 2
+0003 V1 = DO_ICALL
+0004 ASSIGN CV0($array) V1
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T1 = INIT_ARRAY 0 (packed) NEXT
+0007 V2 = FE_RESET_R CV0($array) 0014
+0008 T4 = FE_FETCH_R V2 T3 0014
+0009 INIT_FCALL 1 %d string("stdclass")
+0010 SEND_VAL T3 1
+0011 V3 = DO_UCALL
+0012 T1 = ADD_ARRAY_ELEMENT V3 T4
+0013 JMP 0008
+0014 FE_FREE V2
+0015 FREE T1
+0016 RETURN int(1)
+LIVE RANGES:
+ 1: 0007 - 0015 (tmp/var)
+ 2: 0008 - 0014 (loop)
+ 3: 0009 - 0010 (tmp/var)
+ 4: 0009 - 0012 (tmp/var)
+
+stdClass:
+ ; (lines=3, args=0, vars=0, tmps=1)
+ ; (after optimizer)
+ ; %s
+0000 V0 = NEW 0 string("stdClass")
+0001 DO_FCALL
+0002 RETURN V0
+LIVE RANGES:
+ 0: 0001 - 0002 (new)
diff --git a/ext/standard/tests/array/array_map_foreach_optimization_005.phpt b/ext/standard/tests/array/array_map_foreach_optimization_005.phpt
new file mode 100644
index 00000000000..6f7cf450213
--- /dev/null
+++ b/ext/standard/tests/array/array_map_foreach_optimization_005.phpt
@@ -0,0 +1,84 @@
+--TEST--
+array_map(): foreach optimization - static call
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x20000
+--FILE--
+<?php
+
+class Adder {
+ static function plus1($x) {
+ return $x + 1;
+ }
+}
+
+$array = range(1, 10);
+
+$foo = array_map(Adder::plus1(...), $array);
+
+var_dump($foo);
+
+?>
+--EXPECTF--
+$_main:
+ ; (lines=%d, args=0, vars=%d, tmps=%d)
+ ; (after optimizer)
+ ; %s
+0000 INIT_FCALL 2 %d string("range")
+0001 SEND_VAL int(1) 1
+0002 SEND_VAL int(10) 2
+0003 V2 = DO_ICALL
+0004 ASSIGN CV0($array) V2
+0005 TYPE_ASSERT 131079 string("array_map") CV0($array)
+0006 T2 = INIT_ARRAY 0 (packed) NEXT
+0007 V3 = FE_RESET_R CV0($array) 0014
+0008 T5 = FE_FETCH_R V3 T4 0014
+0009 INIT_STATIC_METHOD_CALL 1 string("Adder") string("plus1")
+0010 SEND_VAL T4 1
+0011 V4 = DO_UCALL
+0012 T2 = ADD_ARRAY_ELEMENT V4 T5
+0013 JMP 0008
+0014 FE_FREE V3
+0015 ASSIGN CV1($foo) T2
+0016 INIT_FCALL 1 %d string("var_dump")
+0017 SEND_VAR CV1($foo) 1
+0018 DO_ICALL
+0019 RETURN int(1)
+LIVE RANGES:
+ 2: 0007 - 0015 (tmp/var)
+ 3: 0008 - 0014 (loop)
+ 4: 0009 - 0010 (tmp/var)
+ 5: 0009 - 0012 (tmp/var)
+
+Adder::plus1:
+ ; (lines=3, args=1, vars=1, tmps=1)
+ ; (after optimizer)
+ ; %s
+0000 CV0($x) = RECV 1
+0001 T1 = ADD CV0($x) int(1)
+0002 RETURN T1
+array(10) {
+ [0]=>
+ int(2)
+ [1]=>
+ int(3)
+ [2]=>
+ int(4)
+ [3]=>
+ int(5)
+ [4]=>
+ int(6)
+ [5]=>
+ int(7)
+ [6]=>
+ int(8)
+ [7]=>
+ int(9)
+ [8]=>
+ int(10)
+ [9]=>
+ int(11)
+}