Skip to content

Commit b362aca

Browse files
committed
PFA: Optimize constant pre-bound arguments
Pre-bound arguments are bound to the generated closure's lexical vars: ``` function f($a, $b) {} $f = f(1, ?); // Generates: $tmp = 1; $f = function ($b) use ($tmp) { return f($tmp, $b); }; ``` Detect which pre-bound arguments are constant and burn them into the generated closure instead: ``` function f($a, $b) {} $f = f(1, ?); // Generates: $f = function ($b) { return f(1, $b); }; ```
1 parent 9391556 commit b362aca

7 files changed

Lines changed: 268 additions & 30 deletions

File tree

Zend/Optimizer/dfa_pass.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,34 @@ static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
469469
}
470470
}
471471
}
472+
473+
if (call_info->caller_call_opline && call_info->caller_call_opline->opcode == ZEND_CALLABLE_CONVERT_PARTIAL) {
474+
/* Build a bitset of constant pre-bound PFA args: These are args whose value is alway the same for all
475+
* instances of a PFA. */
476+
uint32_t const_args = 0;
477+
for (uint32_t i = 0, l = MIN(sizeof(const_args)*CHAR_BIT, call_info->num_args); i < l; i++) {
478+
zend_op *send_opline = call_info->arg_info[i].opline;
479+
if (send_opline->op1_type == IS_CONST) {
480+
zval *value = CT_CONSTANT_EX(op_array, send_opline->op1.constant);
481+
if (Z_TYPE_P(value) == IS_CONSTANT_AST) {
482+
/* Const exprs can evaluate to non-cont zvals (e.g. objects), and are not idempotent */
483+
continue;
484+
}
485+
const_args |= (1 << i);
486+
}
487+
}
488+
489+
/* Pass the bitset to the ZEND_CALLABLE_CONVERT_PARTIAL opline. */
490+
zend_op *call_opline = call_info->caller_call_opline;
491+
if (call_opline->op2_type == IS_UNUSED) {
492+
call_opline->op2.num = const_args;
493+
} else {
494+
ZEND_ASSERT(call_opline->op2_type == IS_CONST);
495+
zval *zv = CT_CONSTANT_EX(op_array, call_opline->op2.constant);
496+
Z_EXTRA_P(zv) = const_args;
497+
}
498+
}
499+
472500
call_info = call_info->next_callee;
473501
} while (call_info);
474502
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
--TEST--
2+
Constant argument optimization
3+
--DESCRIPTION--
4+
Pre-bound arguments that are constant can be burned into the generated
5+
op_array instead of being passed via the Closure's lexical vars.
6+
--ENV--
7+
A=1
8+
--INI--
9+
opcache.enable=1
10+
opcache.enable_cli=1
11+
opcache.optimization_level=-1
12+
--FILE--
13+
<?php
14+
15+
function f($a, $b) {
16+
var_dump([$a, $b]);
17+
}
18+
19+
function g() {
20+
return 3;
21+
}
22+
23+
function h($a, $b, $c) {
24+
var_dump([$a, $b, $c]);
25+
}
26+
27+
function i($a1, $a2, $a3, $a4, $a5, $a6, $a7, $a8, $a9, $a10, $a11, $a12, $a13, $a14, $a15, $a16, $a17, $a18, $a19, $a20, $a21, $a22, $a23, $a24, $a25, $a26, $a27, $a28, $a29, $a30, $a31, $a32, $a33) {
28+
var_dump($a33);
29+
}
30+
31+
function print_lexical_vars($f) {
32+
$vars = new ReflectionFunction($f)->getClosureUsedVariables();
33+
if ($vars === []) {
34+
echo "no lexical vars\n";
35+
} else {
36+
$varNames = array_keys($vars);
37+
echo 'lexical vars: ', implode(', ', $varNames), "\n";
38+
}
39+
}
40+
41+
echo "# Non-constant pre-bound argument:\n";
42+
$f = f(getenv('A'), ?);
43+
print_lexical_vars($f);
44+
$f(2);
45+
46+
echo "# Constant pre-bound argument:\n";
47+
$f = f(2, ?);
48+
print_lexical_vars($f);
49+
$f(2);
50+
51+
echo "# Constant pre-bound argument (inverted):\n";
52+
$f = f(?, 2);
53+
print_lexical_vars($f);
54+
$f(2);
55+
56+
echo "# Inlined pre-bound argument:\n";
57+
$f = f(g(), ?);
58+
print_lexical_vars($f);
59+
$f(2);
60+
61+
echo "# Constexpr pre-bound argument:\n";
62+
const B = 4;
63+
$f = f(B, ?);
64+
print_lexical_vars($f);
65+
$f(2);
66+
67+
echo "# Mixed arguments:\n";
68+
$f = h(5, getenv('A'), ?);
69+
print_lexical_vars($f);
70+
$f(2);
71+
72+
echo "# Constant array pre-bound argument:\n";
73+
$f = f(['foo' => 'bar'], ?);
74+
print_lexical_vars($f);
75+
$f(2);
76+
77+
echo "# Named arguments:\n";
78+
$f = h(1, c: ?, b: ?);
79+
print_lexical_vars($f);
80+
$f(2, 3);
81+
82+
echo "# Many arguments (optimization can not be applied for all args) :\n";
83+
$f = i(?, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33);
84+
print_lexical_vars($f);
85+
$f(33);
86+
87+
?>
88+
--EXPECT--
89+
# Non-constant pre-bound argument:
90+
lexical vars: a
91+
array(2) {
92+
[0]=>
93+
string(1) "1"
94+
[1]=>
95+
int(2)
96+
}
97+
# Constant pre-bound argument:
98+
no lexical vars
99+
array(2) {
100+
[0]=>
101+
int(2)
102+
[1]=>
103+
int(2)
104+
}
105+
# Constant pre-bound argument (inverted):
106+
no lexical vars
107+
array(2) {
108+
[0]=>
109+
int(2)
110+
[1]=>
111+
int(2)
112+
}
113+
# Inlined pre-bound argument:
114+
no lexical vars
115+
array(2) {
116+
[0]=>
117+
int(3)
118+
[1]=>
119+
int(2)
120+
}
121+
# Constexpr pre-bound argument:
122+
lexical vars: a
123+
array(2) {
124+
[0]=>
125+
int(4)
126+
[1]=>
127+
int(2)
128+
}
129+
# Mixed arguments:
130+
lexical vars: b
131+
array(3) {
132+
[0]=>
133+
int(5)
134+
[1]=>
135+
string(1) "1"
136+
[2]=>
137+
int(2)
138+
}
139+
# Constant array pre-bound argument:
140+
no lexical vars
141+
array(2) {
142+
[0]=>
143+
array(1) {
144+
["foo"]=>
145+
string(3) "bar"
146+
}
147+
[1]=>
148+
int(2)
149+
}
150+
# Named arguments:
151+
no lexical vars
152+
array(3) {
153+
[0]=>
154+
int(1)
155+
[1]=>
156+
int(3)
157+
[2]=>
158+
int(2)
159+
}
160+
# Many arguments (optimization can not be applied for all args) :
161+
lexical vars: a33
162+
int(33)

Zend/zend_ast.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,7 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
13531353
ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1),
13541354
extra_named_params, named_positions,
13551355
fcc_ast->filename, &ast->lineno,
1356-
(void**)cache_slot, fcc_ast->name, flags);
1356+
(void**)cache_slot, fcc_ast->name, flags, 0);
13571357

13581358
if (named_positions) {
13591359
zend_array_release(named_positions);

Zend/zend_partial.c

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ static zend_always_inline bool zp_is_non_static_closure(const zend_function *fun
6666
return ((function->common.fn_flags & (ZEND_ACC_STATIC|ZEND_ACC_CLOSURE)) == ZEND_ACC_CLOSURE);
6767
}
6868

69+
/* Whether argument at offset 'offset' is const. Such arguments can be burned into the generated op_array */
70+
static inline bool zp_is_const_arg(uint32_t const_args, uint32_t offset) {
71+
if (offset < sizeof(const_args) * CHAR_BIT) {
72+
return const_args & (1 << offset);
73+
}
74+
return false;
75+
}
76+
6977
static zend_never_inline ZEND_COLD void zp_args_underflow(
7078
const zend_function *function, uint32_t args, uint32_t expected)
7179
{
@@ -179,7 +187,7 @@ static zend_string *zp_get_func_param_name(const zend_function *function, uint32
179187
* including params and used vars. */
180188
static zp_names *zp_assign_names(uint32_t argc, zval *argv,
181189
zend_function *function, bool variadic_partial,
182-
zend_array *extra_named_params)
190+
zend_array *extra_named_params, uint32_t const_args)
183191
{
184192
zp_names *names = zend_arena_calloc(&CG(ast_arena),
185193
1, zend_safe_address_guarded(argc, sizeof(*names->params), offsetof(zp_names, params)));
@@ -226,7 +234,7 @@ static zp_names *zp_assign_names(uint32_t argc, zval *argv,
226234
/* Assign names for pre-bound params (lexical vars).
227235
* There may be clashes, we ensure to generate unique names. */
228236
for (uint32_t offset = 0; offset < argc; offset++) {
229-
if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset])) {
237+
if (Z_IS_PLACEHOLDER_P(&argv[offset]) || Z_ISUNDEF(argv[offset]) || zp_is_const_arg(const_args, offset)) {
230238
continue;
231239
}
232240
uint32_t n = 2;
@@ -510,7 +518,7 @@ static zend_ast *zp_compile_forwarding_call(
510518
zp_names *var_names, bool uses_variadic_placeholder, uint32_t num_args,
511519
zend_class_entry *called_scope, zend_type return_type,
512520
bool forward_superfluous_args,
513-
zend_ast *stmts_ast)
521+
zend_ast *stmts_ast, uint32_t const_args)
514522
{
515523
bool is_assert = zend_string_equals(function->common.function_name,
516524
ZSTR_KNOWN(ZEND_STR_ASSERT));
@@ -551,6 +559,9 @@ static zend_ast *zp_compile_forwarding_call(
551559
default_value_ast = zend_ast_create_zval(&default_value);
552560
}
553561
args_ast = zend_ast_list_add(args_ast, default_value_ast);
562+
} else if (zp_is_const_arg(const_args, offset)) {
563+
ZEND_ASSERT(Z_TYPE(argv[offset]) < IS_OBJECT);
564+
args_ast = zend_ast_list_add(args_ast, zend_ast_create_zval(&argv[offset]));
554565
} else {
555566
args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_VAR,
556567
zend_ast_create_zval_from_str(zend_string_copy(var_names->params[offset]))));
@@ -661,7 +672,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
661672
const zend_array *named_positions,
662673
zend_string *declaring_filename,
663674
const uint32_t *declaring_lineno_ptr, void **cache_slot,
664-
zend_string *pfa_name, uint32_t flags) {
675+
zend_string *pfa_name, uint32_t flags, uint32_t const_args) {
665676

666677
zend_op_array *op_array = NULL;
667678

@@ -771,7 +782,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
771782
/* Assign variable names */
772783

773784
zp_names *var_names = zp_assign_names(argc, argv, function,
774-
uses_variadic_placeholder, extra_named_params);
785+
uses_variadic_placeholder, extra_named_params, const_args);
775786

776787
/* Generate AST */
777788

@@ -825,15 +836,17 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
825836
default_value_ast, attributes_ast, NULL, NULL);
826837

827838
} else if (!Z_ISUNDEF(argv[offset])) {
828-
// TODO: If the pre-bound parameter is a literal, it can be a
829-
// literal in the function body instead of a lexical var.
830-
zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
831-
zend_string_copy(var_names->params[offset]));
832-
if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
833-
lexical_var_ast->attr = ZEND_BIND_REF;
839+
if (zp_is_const_arg(const_args, offset)) {
840+
/* Will be burned into the op_array */
841+
} else {
842+
zend_ast *lexical_var_ast = zend_ast_create_zval_from_str(
843+
zend_string_copy(var_names->params[offset]));
844+
if (zp_arg_must_be_sent_by_ref(function, offset+1)) {
845+
lexical_var_ast->attr = ZEND_BIND_REF;
846+
}
847+
lexical_vars_ast = zend_ast_list_add(
848+
lexical_vars_ast, lexical_var_ast);
834849
}
835-
lexical_vars_ast = zend_ast_list_add(
836-
lexical_vars_ast, lexical_var_ast);
837850
}
838851
}
839852

@@ -894,7 +907,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
894907
no_forwarding_ast = zp_compile_forwarding_call(this_ptr, function,
895908
argc, argv, extra_named_params,
896909
var_names, uses_variadic_placeholder, num_params,
897-
called_scope, return_type, false, no_forwarding_ast);
910+
called_scope, return_type, false, no_forwarding_ast, const_args);
898911

899912
if (!no_forwarding_ast) {
900913
ZEND_ASSERT(EG(exception));
@@ -904,7 +917,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
904917
forwarding_ast = zp_compile_forwarding_call(this_ptr, function,
905918
argc, argv, extra_named_params,
906919
var_names, uses_variadic_placeholder, num_params,
907-
called_scope, return_type, true, forwarding_ast);
920+
called_scope, return_type, true, forwarding_ast, const_args);
908921

909922
if (!forwarding_ast) {
910923
ZEND_ASSERT(EG(exception));
@@ -931,7 +944,7 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
931944
stmts_ast = zp_compile_forwarding_call(this_ptr, function,
932945
argc, argv, extra_named_params,
933946
var_names, uses_variadic_placeholder, num_params,
934-
called_scope, return_type, false, stmts_ast);
947+
called_scope, return_type, false, stmts_ast, const_args);
935948

936949
if (!stmts_ast) {
937950
ZEND_ASSERT(EG(exception));
@@ -1000,7 +1013,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
10001013
const zend_array *named_positions,
10011014
zend_string *declaring_filename,
10021015
const uint32_t *declaring_lineno_ptr, void **cache_slot,
1003-
zend_string *pfa_name, uint32_t flags) {
1016+
zend_string *pfa_name, uint32_t flags, uint32_t const_args) {
10041017

10051018
if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION
10061019
? cache_slot[0] == function
@@ -1015,7 +1028,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
10151028
if (UNEXPECTED(!op_array)) {
10161029
op_array = zp_compile(this_ptr, function, argc, argv,
10171030
extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr,
1018-
cache_slot, pfa_name, flags);
1031+
cache_slot, pfa_name, flags, const_args);
10191032
}
10201033

10211034
if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) {
@@ -1037,7 +1050,7 @@ static void zp_free_unbound_args(uint32_t start, uint32_t argc, zval *argv)
10371050

10381051
/* Bind pre-bound arguments as lexical vars */
10391052
static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *argv,
1040-
zend_array *extra_named_params) {
1053+
zend_array *extra_named_params, uint32_t const_args) {
10411054

10421055
zend_arg_info *arg_infos = function->common.arg_info;
10431056
uint32_t bind_offset = 0;
@@ -1052,7 +1065,7 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *
10521065

10531066
for (uint32_t offset = 0; offset < argc; offset++) {
10541067
zval *var = &argv[offset];
1055-
if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var)) {
1068+
if (Z_IS_PLACEHOLDER_P(var) || Z_ISUNDEF_P(var) || zp_is_const_arg(const_args, offset)) {
10561069
continue;
10571070
}
10581071
zend_arg_info *arg_info;
@@ -1095,14 +1108,14 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
10951108
const zend_array *named_positions,
10961109
zend_string *declaring_filename,
10971110
const uint32_t *declaring_lineno_ptr, void **cache_slot,
1098-
zend_string *pfa_name, uint32_t flags) {
1111+
zend_string *pfa_name, uint32_t flags, uint32_t const_args) {
10991112

11001113
ZEND_ASSERT(pfa_name);
11011114

11021115
const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv,
11031116
extra_named_params, named_positions,
11041117
declaring_filename, declaring_lineno_ptr,
1105-
cache_slot, pfa_name, flags);
1118+
cache_slot, pfa_name, flags, const_args);
11061119

11071120
if (UNEXPECTED(!op_array)) {
11081121
ZEND_ASSERT(EG(exception));
@@ -1130,7 +1143,7 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
11301143
function->common.scope, called_scope, &object,
11311144
(function->common.fn_flags & ZEND_ACC_CLOSURE) != 0);
11321145

1133-
zp_bind(result, function, argc, argv, extra_named_params);
1146+
zp_bind(result, function, argc, argv, extra_named_params, const_args);
11341147
}
11351148

11361149
void zend_partial_op_array_dtor(zval *pDest)

0 commit comments

Comments
 (0)