Skip to content

Commit a3576bd

Browse files
authored
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
1 parent e9e0fe4 commit a3576bd

14 files changed

+1163
-573
lines changed

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ PHP 8.6 UPGRADE NOTES
151151
parsing the format string.
152152
. Arguments are now passed more efficiently to known constructors (e.g. when
153153
using new self()).
154+
. array_map() using a first-class callable or partial function application
155+
callback will be compiled into the equivalent foreach-loop, avoiding the
156+
creation of intermediate Closures, the overhead of calling userland
157+
callbacks from internal functions and providing for better insight for the
158+
JIT.
154159

155160
- DOM:
156161
. Made splitText() faster and consume less memory.

Zend/tests/functions/zend_call_function_deprecated_frame.phpt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ var_dump($a);
1919
--EXPECTF--
2020
Fatal error: Uncaught Exception: Function foo() is deprecated in %s:%d
2121
Stack trace:
22-
#0 [internal function]: {closure:%s:%d}(16384, 'Function foo() ...', '%s', %d)
23-
#1 %s(%d): array_map(Object(Closure), Array)
24-
#2 {main}
22+
#0 %s(%d): {closure:%s:%d}(16384, 'Function foo() ...', '%s', %d)
23+
#1 {main}
2524
thrown in %s on line %d

Zend/tests/gh14003.phpt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ array_filter(
1818
--EXPECTF--
1919
Fatal error: Uncaught Exception: Test in %s:%d
2020
Stack trace:
21-
#0 [internal function]: foo('a')
22-
#1 %s(%d): array_map(Object(Closure), Array)
23-
#2 {main}
21+
#0 %s(%d): foo('a')
22+
#1 {main}
2423
thrown in %s on line %d

Zend/zend_compile.c

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5027,7 +5027,115 @@ static zend_result zend_compile_func_clone(znode *result, const zend_ast_list *a
50275027
return SUCCESS;
50285028
}
50295029

5030-
static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, uint32_t type) /* {{{ */
5030+
static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *args, zend_string *lcname, uint32_t lineno) /* {{{ */
5031+
{
5032+
/* Bail out if we do not have exactly two parameters. */
5033+
if (args->children != 2) {
5034+
return FAILURE;
5035+
}
5036+
5037+
zend_ast *callback = args->child[0];
5038+
5039+
/* Bail out if the callback is not a FCC/PFA. */
5040+
zend_ast *args_ast;
5041+
switch (callback->kind) {
5042+
case ZEND_AST_CALL:
5043+
case ZEND_AST_STATIC_CALL:
5044+
args_ast = zend_ast_call_get_args(callback);
5045+
if (args_ast->kind != ZEND_AST_CALLABLE_CONVERT) {
5046+
return FAILURE;
5047+
}
5048+
5049+
break;
5050+
default:
5051+
return FAILURE;
5052+
}
5053+
5054+
/* Bail out if the callback is assert() due to the AST stringification logic
5055+
* breaking for the generated call.
5056+
*/
5057+
if (callback->kind == ZEND_AST_CALL && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) {
5058+
return FAILURE;
5059+
}
5060+
5061+
znode value;
5062+
value.op_type = IS_TMP_VAR;
5063+
value.u.op.var = get_temporary_variable();
5064+
5065+
zend_ast_list *callback_args = zend_ast_get_list(((zend_ast_fcc*)args_ast)->args);
5066+
zend_ast *call_args = zend_ast_create_list(0, ZEND_AST_ARG_LIST);
5067+
for (uint32_t i = 0; i < callback_args->children; i++) {
5068+
zend_ast *child = callback_args->child[i];
5069+
if (child->kind == ZEND_AST_PLACEHOLDER_ARG) {
5070+
call_args = zend_ast_list_add(call_args, zend_ast_create_znode(&value));
5071+
} else {
5072+
ZEND_ASSERT(0 && "not implemented");
5073+
call_args = zend_ast_list_add(call_args, child);
5074+
}
5075+
}
5076+
5077+
zend_op *opline;
5078+
5079+
znode array;
5080+
zend_compile_expr(&array, args->child[1]);
5081+
/* array is an argument to both ZEND_TYPE_ASSERT and to ZEND_FE_RESET_R. */
5082+
if (array.op_type == IS_CONST) {
5083+
Z_TRY_ADDREF(array.u.constant);
5084+
}
5085+
5086+
/* Verify that the input array actually is an array. */
5087+
znode name;
5088+
name.op_type = IS_CONST;
5089+
ZVAL_STR_COPY(&name.u.constant, lcname);
5090+
opline = zend_emit_op(NULL, ZEND_TYPE_ASSERT, &name, &array);
5091+
opline->lineno = lineno;
5092+
opline->extended_value = (2 << 16) | IS_ARRAY;
5093+
const zval *fbc_zv = zend_hash_find(CG(function_table), lcname);
5094+
const Bucket *fbc_bucket = (const Bucket*)((uintptr_t)fbc_zv - XtOffsetOf(Bucket, val));
5095+
Z_EXTRA_P(CT_CONSTANT(opline->op1)) = fbc_bucket - CG(function_table)->arData;
5096+
5097+
/* Initialize the result array. */
5098+
zend_emit_op_tmp(result, ZEND_INIT_ARRAY, NULL, NULL);
5099+
5100+
/* foreach loop starts here. */
5101+
znode key;
5102+
5103+
uint32_t opnum_reset = get_next_op_number();
5104+
znode reset_node;
5105+
zend_emit_op(&reset_node, ZEND_FE_RESET_R, &array, NULL);
5106+
zend_begin_loop(ZEND_FE_FREE, &reset_node, false);
5107+
uint32_t opnum_fetch = get_next_op_number();
5108+
zend_emit_op_tmp(&key, ZEND_FE_FETCH_R, &reset_node, &value);
5109+
5110+
/* loop body */
5111+
znode call_result;
5112+
switch (callback->kind) {
5113+
case ZEND_AST_CALL:
5114+
zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_CALL, callback->child[0], call_args));
5115+
break;
5116+
case ZEND_AST_STATIC_CALL:
5117+
zend_compile_expr(&call_result, zend_ast_create(ZEND_AST_STATIC_CALL, callback->child[0], callback->child[1], call_args));
5118+
break;
5119+
}
5120+
opline = zend_emit_op(NULL, ZEND_ADD_ARRAY_ELEMENT, &call_result, &key);
5121+
SET_NODE(opline->result, result);
5122+
/* end loop body */
5123+
5124+
zend_emit_jump(opnum_fetch);
5125+
5126+
uint32_t opnum_loop_end = get_next_op_number();
5127+
opline = &CG(active_op_array)->opcodes[opnum_reset];
5128+
opline->op2.opline_num = opnum_loop_end;
5129+
opline = &CG(active_op_array)->opcodes[opnum_fetch];
5130+
opline->extended_value = opnum_loop_end;
5131+
5132+
zend_end_loop(opnum_fetch, &reset_node);
5133+
zend_emit_op(NULL, ZEND_FE_FREE, &reset_node, NULL);
5134+
5135+
return SUCCESS;
5136+
}
5137+
5138+
static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, uint32_t type, uint32_t lineno) /* {{{ */
50315139
{
50325140
if (zend_string_equals_literal(lcname, "strlen")) {
50335141
return zend_compile_func_strlen(result, args);
@@ -5099,12 +5207,14 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *
50995207
return zend_compile_func_printf(result, args);
51005208
} else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) {
51015209
return zend_compile_func_clone(result, args);
5210+
} else if (zend_string_equals_literal(lcname, "array_map")) {
5211+
return zend_compile_func_array_map(result, args, lcname, lineno);
51025212
} else {
51035213
return FAILURE;
51045214
}
51055215
}
51065216

5107-
static zend_result zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, const zend_function *fbc, uint32_t type) /* {{{ */
5217+
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) /* {{{ */
51085218
{
51095219
if (CG(compiler_options) & ZEND_COMPILE_NO_BUILTINS) {
51105220
return FAILURE;
@@ -5120,7 +5230,7 @@ static zend_result zend_try_compile_special_func(znode *result, zend_string *lcn
51205230
return FAILURE;
51215231
}
51225232

5123-
if (zend_try_compile_special_func_ex(result, lcname, args, type) == SUCCESS) {
5233+
if (zend_try_compile_special_func_ex(result, lcname, args, type, lineno) == SUCCESS) {
51245234
return SUCCESS;
51255235
}
51265236

@@ -5263,7 +5373,7 @@ static void zend_compile_call(znode *result, const zend_ast *ast, uint32_t type)
52635373

52645374
if (!is_callable_convert &&
52655375
zend_try_compile_special_func(result, lcname,
5266-
zend_ast_get_list(args_ast), fbc, type) == SUCCESS
5376+
zend_ast_get_list(args_ast), fbc, type, ast->lineno) == SUCCESS
52675377
) {
52685378
zend_string_release_ex(lcname, 0);
52695379
zval_ptr_dtor(&name_node.u.constant);

Zend/zend_vm_def.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8846,6 +8846,38 @@ ZEND_VM_C_LABEL(type_check_resource):
88468846
}
88478847
}
88488848

8849+
ZEND_VM_HOT_HANDLER(211, ZEND_TYPE_ASSERT, CONST, ANY, NUM)
8850+
{
8851+
USE_OPLINE
8852+
SAVE_OPLINE();
8853+
8854+
zval *value = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
8855+
8856+
uint8_t actual_type = Z_TYPE_P(value);
8857+
uint8_t expected_type = opline->extended_value & 0xff;
8858+
/* Simple types can be checked directly. */
8859+
if (UNEXPECTED(actual_type != expected_type)) {
8860+
zend_function *fbc;
8861+
{
8862+
zval *fname = (zval*)RT_CONSTANT(opline, opline->op1);
8863+
ZEND_ASSERT(Z_EXTRA_P(fname) != 0);
8864+
fbc = Z_FUNC(EG(function_table)->arData[Z_EXTRA_P(fname)].val);
8865+
ZEND_ASSERT(fbc->type != ZEND_USER_FUNCTION);
8866+
}
8867+
uint16_t argno = opline->extended_value >> 16;
8868+
zend_arg_info *arginfo = &fbc->common.arg_info[argno - 1];
8869+
8870+
if (!zend_check_type(&arginfo->type, value, /* is_return_type */ false, /* is_internal */ true)) {
8871+
const char *param_name = get_function_arg_name(fbc, argno);
8872+
zend_string *expected = zend_type_to_string(arginfo->type);
8873+
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));
8874+
zend_string_release(expected);
8875+
}
8876+
}
8877+
8878+
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
8879+
}
8880+
88498881
ZEND_VM_HOT_HANDLER(122, ZEND_DEFINED, CONST, ANY, CACHE_SLOT)
88508882
{
88518883
USE_OPLINE

0 commit comments

Comments
 (0)