Skip to content

Commit edc169e

Browse files
authored
[PFA 4/n] Optimize constant pre-bound arguments (#22829)
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 a7e0ddb commit edc169e

13 files changed

Lines changed: 311 additions & 46 deletions

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-const zvals (e.g. objects), and are not idempotent */
483+
continue;
484+
}
485+
const_args |= (UINT32_C(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(1);
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(1)
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/tests/partial_application/references_004.phpt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
--TEST--
22
PFA receives variadic param by ref if the actual function does
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.file_update_protection=0
38
--FILE--
49
<?php
510

@@ -26,9 +31,8 @@ var_dump($b, $c, $d);
2631
Closure [ <user> static function {closure:%s:%d} ] {
2732
@@ %sreferences_004.php 13 - 13
2833

29-
- Bound Variables [2] {
30-
Variable #0 [ $a ]
31-
Variable #1 [ $args2 ]
34+
- Bound Variables [1] {
35+
Variable #0 [ $args2 ]
3236
}
3337

3438
- Parameters [2] {

Zend/tests/partial_application/variation_debug_001.phpt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
--TEST--
22
PFA variation: var_dump(), user function
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.file_update_protection=0
8+
--ENV--
9+
A=20
310
--FILE--
411
<?php
512
function bar($a = 1, $b = 2, ...$c) {
613

714
}
815

9-
var_dump(bar(?, new stdClass, 20, new stdClass, four: 4));
16+
var_dump(bar(?, new stdClass, (int)getenv('A'), new stdClass, four: 4));
1017
?>
1118
--EXPECTF--
1219
object(Closure)#%d (5) {

Zend/tests/partial_application/variation_debug_002.phpt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
--TEST--
22
PFA variation: var_dump(), internal function
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.file_update_protection=0
8+
--ENV--
9+
A=1
310
--FILE--
411
<?php
5-
var_dump(array_map(?, [1, 2, 3], [4, 5, 6], four: new stdClass, ...));
12+
13+
function notconst($value) {
14+
return getenv('A') ? $value : null;
15+
}
16+
17+
var_dump(array_map(?, notconst([1, 2, 3]), notconst([4, 5, 6]), four: new stdClass, ...));
618
?>
719
--EXPECTF--
820
object(Closure)#%d (5) {
@@ -11,7 +23,7 @@ object(Closure)#%d (5) {
1123
["file"]=>
1224
string(%d) "%svariation_debug_002.php"
1325
["line"]=>
14-
int(2)
26+
int(7)
1527
["static"]=>
1628
array(3) {
1729
["array"]=>

Zend/tests/partial_application/variation_variadics_001.phpt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
--TEST--
22
PFA variation: variadics, user function
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.file_update_protection=0
8+
--ENV--
9+
A=1
310
--FILE--
411
<?php
512
function foo($a, ...$b) {
@@ -16,11 +23,6 @@ $foo(1000, 10000);
1623
Closure [ <user> static function {closure:%s:%d} ] {
1724
@@ %s 6 - 6
1825

19-
- Bound Variables [2] {
20-
Variable #0 [ $a ]
21-
Variable #1 [ $b2 ]
22-
}
23-
2426
- Parameters [1] {
2527
Parameter #0 [ <optional> ...$b ]
2628
}

Zend/tests/partial_application/variation_variadics_002.phpt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
--TEST--
22
PFA variation: variadics, internal function
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.optimization_level=-1
7+
opcache.file_update_protection=0
38
--FILE--
49
<?php
510
$sprintf = sprintf("%d %d %d", 100, ...);
@@ -12,11 +17,6 @@ echo $sprintf(1000, 10000);
1217
Closure [ <user> static function {closure:%s:%d} ] {
1318
@@ %svariation_variadics_002.php 2 - 2
1419

15-
- Bound Variables [2] {
16-
Variable #0 [ $format ]
17-
Variable #1 [ $values2 ]
18-
}
19-
2020
- Parameters [1] {
2121
Parameter #0 [ <optional> mixed ...$values ]
2222
}

Zend/zend_ast.c

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

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

Zend/zend_compile.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4162,6 +4162,8 @@ static void zend_compile_call_partial(znode *result, zend_ast_fcc *fcc_ast, uint
41624162
if (!Z_ISUNDEF_P(named_positions)) {
41634163
opline->op2.constant = zend_add_literal(named_positions);
41644164
opline->op2_type = IS_CONST;
4165+
} else {
4166+
opline->op2.num = 0;
41654167
}
41664168
}
41674169

0 commit comments

Comments
 (0)