From 86a9f1ed916c25e386b24017e80625bc4b6755ea Mon Sep 17 00:00:00 2001 From: Artem Ukrainskiy Date: Wed, 7 Oct 2026 18:11:39 +0300 Subject: [PATCH] Zend: Compile a list assignment from an array literal without the array [$a, $b] = [$b, $a]; built an array of the values, fetched every element out of it again and freed it. When the result of the assignment is not used, the right side is an array literal and the left side a flat list with a target for every value, assign the values to the targets directly instead. The values are still evaluated in order before the first assignment and a variable among them is copied first. Each target then takes its value, so a value its target does not keep (a setter that drops it, the same variable twice, a typed property that coerces it) is released at that assignment rather than after the last one, and a destructor that throws there stops the rest of the statement. Assignments in the expression lists of a for loop, whose results are not used either, are compiled the same way. Nested lists, keys on either side, references, spread, a different number of values and targets and a used result keep the usual compilation. A swap goes from 922 to 467 instructions per call of a function doing it, below a swap through a temporary variable (493). --- NEWS | 5 + UPGRADING | 8 ++ Zend/tests/list/list_from_array_literal.phpt | 110 +++++++++++++++++ .../list/list_from_array_literal_order.inc | 116 ++++++++++++++++++ .../list/list_from_array_literal_order.phpt | 87 +++++++++++++ .../list_from_array_literal_order_opt.phpt | 93 ++++++++++++++ .../list_from_array_literal_undefined.phpt | 31 +++++ Zend/zend_compile.c | 77 +++++++++++- .../opt/list_assign_from_array_literal.phpt | 93 ++++++++++++++ 9 files changed, 616 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/list/list_from_array_literal.phpt create mode 100644 Zend/tests/list/list_from_array_literal_order.inc create mode 100644 Zend/tests/list/list_from_array_literal_order.phpt create mode 100644 Zend/tests/list/list_from_array_literal_order_opt.phpt create mode 100644 Zend/tests/list/list_from_array_literal_undefined.phpt create mode 100644 ext/opcache/tests/opt/list_assign_from_array_literal.phpt diff --git a/NEWS b/NEWS index 4a9a2135ee29..37f2b5a696ea 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,11 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.7.0alpha1 +- Core: + . A flat unkeyed list assignment from an array literal with one value per + target, such as [$a, $b] = [$b, $a], no longer builds the array when the + result is unused (GH-23048). (Artem Ukrainskiy) + - Curl: . curl_multi_exec() now throws a TypeError when still_running is not an int. (arshidkv12) diff --git a/UPGRADING b/UPGRADING index 9442aad84504..7ade3686c6ef 100644 --- a/UPGRADING +++ b/UPGRADING @@ -117,6 +117,14 @@ PHP 8.7 UPGRADE NOTES 14. Performance Improvements ======================================== +- Core: + . A flat unkeyed list assignment from an array literal with one value per + target, such as [$a, $b] = [$b, $a], no longer builds the array when the + result is unused: the values are evaluated in order, a variable among them + is copied, and they are assigned to the targets directly. A value its + target does not keep is now released at that assignment instead of after + the last one. + - Standard: . Improved performance of strcspn() and strpbrk() when searching longer strings with a single-byte character mask. diff --git a/Zend/tests/list/list_from_array_literal.phpt b/Zend/tests/list/list_from_array_literal.phpt new file mode 100644 index 000000000000..9c3fbddbe81a --- /dev/null +++ b/Zend/tests/list/list_from_array_literal.phpt @@ -0,0 +1,110 @@ +--TEST-- +List assignment from an array literal: values and targets +--FILE-- +a, $p->b] = [$p->b, $p->a]; +[P::$c, P::$d] = [P::$d, P::$c]; +var_dump($p->a, $p->b, P::$c, P::$d); + +$x = [1, 2]; +[$x[0], $x[1]] = [$x[1], $x[0]]; +var_dump($x); + +[$GLOBALS['g']] = [5]; +var_dump($g); + +$a = 1; $b = 2; +[$a, $b] = [1 => 'one', 0 => 'zero']; +var_dump($a, $b); + +$a = 1; $b = 2; +[[$a, $b], $c] = [[$b, $a], 3]; +var_dump($a, $b, $c); + +$r = [$a, $b] = [7, 8]; +var_dump($r, $a, $b); + +$a = 1; +[$f, $a] = [fn() => $a, 2]; +var_dump($f()); + +for ([$a, $b] = [1, 2], $i = 0; $i < 3; $i++, [$a, $b] = [$b, $a + $b]) { + echo "$a $b\n"; +} +for ($i = 0; [$a, $b] = [$b, $a], $i < 2; $i++) { + echo "$a $b\n"; +} +for ($i = 0; [$x, $y] = [$i, $i * 2]; $i++) { + if ($i > 1) break; + echo "$x $y\n"; +} + +?> +--EXPECT-- +int(2) +int(1) +int(1) +int(3) +int(2) +int(3) +int(1) +int(2) +int(2) +int(1) +int(1) +int(2) +int(2) +int(1) +int(4) +int(3) +array(2) { + [0]=> + int(2) + [1]=> + int(1) +} +int(5) +string(4) "zero" +string(3) "one" +int(2) +int(1) +int(3) +array(2) { + [0]=> + int(7) + [1]=> + int(8) +} +int(7) +int(8) +int(1) +1 2 +2 3 +3 5 +8 5 +5 8 +0 0 +1 2 diff --git a/Zend/tests/list/list_from_array_literal_order.inc b/Zend/tests/list/list_from_array_literal_order.inc new file mode 100644 index 000000000000..0331ff015bb1 --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_order.inc @@ -0,0 +1,116 @@ +n\n"; } + public function __destruct() { echo "destruct $this->n\n"; $GLOBALS['dead'] = true; } + public function __toString(): string { return $this->n; } +} +function v(string $n) { echo "value $n\n"; return $n; } +function k(string $n) { echo "key $n\n"; return 0; } + +echo "-- values before targets\n"; +$x = []; +[$x[k('a')], $x[k('b')]] = [v('1'), v('2')]; + +echo "-- extra values\n"; +[$a] = [new D('a'), new D('x'), new D('y')]; +echo "assigned\n"; +unset($a); + +echo "-- values nobody takes live until the assignments are done\n"; +function untaken_variable() { + $x = new D('old x'); + $y = new D('y'); + [, $x, $y] = [$x, 1, 2]; + echo "assigned\n"; +} +untaken_variable(); + +echo "-- a value the target does not keep is released at its assignment\n"; +final class Drop implements ArrayAccess { + public function offsetExists($o): bool { return true; } + public function offsetGet($o): mixed { return null; } + public function offsetSet($o, $v): void { echo "set $o\n"; } + public function offsetUnset($o): void {} +} +$drop = new Drop; +[$drop['k'], $drop['l']] = [new D('k'), new D('l')]; +echo "assigned\n"; +$b = new D('old b'); +[$a, $a, $b] = [new D('first'), new D('second'), 3]; +echo "assigned\n"; +unset($a); + +echo "-- the same with a destructor that throws\n"; +final class Boom { public function __destruct() { throw new Exception('boom'); } } +$b = 0; +try { + [$a, $a, $b] = [new Boom, new stdClass, 42]; +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} +var_dump($b); +unset($a); + +echo "-- two targets that are references to each other\n"; +$a = null; $b = null; $c = 0; +$a = &$b; +try { + [$a, $b, $c] = [new Boom, new stdClass, 42]; +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} +var_dump($c); +unset($a, $b); + +echo "-- a variable that is a reference to a typed property\n"; +final class S { public string $s = ''; } +$obj = new S; +$r = &$obj->s; +[$r, $drop['y']] = [new D('coerced'), 1]; +echo "assigned\n"; +unset($r, $obj); + +echo "-- pending values when a target expression throws\n"; +function boom() { throw new Exception('boom'); } +try { + [$arr[boom()], $b, $c] = [new D('v1'), new D('v2'), new D('v3')]; +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +echo "-- a variable is copied before any assignment\n"; +$o = new D('o'); +[$o, $p] = [new D('new o'), $o]; +echo "assigned\n"; +var_dump($o->n, $p->n); +unset($o, $p); + +echo "-- exception in a later value\n"; +try { + [$a, $b, $c] = [new D('first'), new D('second'), boom()]; +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +echo "-- exception in an assignment\n"; +class T { public int $i = 1; } +$t = new T; +try { + [$t->i, $b] = ['not an int', new D('after the throw')]; +} catch (TypeError $e) { + echo get_class($e), "\n"; +} +var_dump($t->i); + +echo "-- a destructor that throws when a target drops the value\n"; +final class BoomNamed extends D { + public function __destruct() { echo "destruct $this->n throws\n"; throw new Exception("boom $this->n"); } +} +try { + [$drop['a'], $drop['b']] = [new BoomNamed('a'), new D('b')]; +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +echo "done\n"; diff --git a/Zend/tests/list/list_from_array_literal_order.phpt b/Zend/tests/list/list_from_array_literal_order.phpt new file mode 100644 index 000000000000..db8f9e71d54a --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_order.phpt @@ -0,0 +1,87 @@ +--TEST-- +List assignment from an array literal: evaluation order and lifetime of the values +--FILE-- + +--EXPECT-- +-- values before targets +value 1 +value 2 +key a +key b +-- extra values +construct a +construct x +construct y +destruct x +destruct y +assigned +destruct a +-- values nobody takes live until the assignments are done +construct old x +construct y +destruct y +destruct old x +assigned +-- a value the target does not keep is released at its assignment +construct k +construct l +set k +destruct k +set l +destruct l +assigned +construct old b +construct first +construct second +destruct first +destruct old b +assigned +destruct second +-- the same with a destructor that throws +boom +int(0) +-- two targets that are references to each other +boom +int(0) +-- a variable that is a reference to a typed property +construct coerced +destruct coerced +set y +assigned +-- pending values when a target expression throws +construct v1 +construct v2 +construct v3 +destruct v1 +destruct v2 +destruct v3 +boom +-- a variable is copied before any assignment +construct o +construct new o +assigned +string(5) "new o" +string(1) "o" +destruct new o +destruct o +-- exception in a later value +construct first +construct second +destruct first +destruct second +boom +-- exception in an assignment +construct after the throw +destruct after the throw +TypeError +int(1) +-- a destructor that throws when a target drops the value +construct a +construct b +set a +destruct a throws +destruct b +boom a +done diff --git a/Zend/tests/list/list_from_array_literal_order_opt.phpt b/Zend/tests/list/list_from_array_literal_order_opt.phpt new file mode 100644 index 000000000000..110032797ae6 --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_order_opt.phpt @@ -0,0 +1,93 @@ +--TEST-- +List assignment from an array literal: evaluation order and lifetime of the values, with the optimizer +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +--FILE-- + +--EXPECT-- +-- values before targets +value 1 +value 2 +key a +key b +-- extra values +construct a +construct x +construct y +destruct x +destruct y +assigned +destruct a +-- values nobody takes live until the assignments are done +construct old x +construct y +destruct y +destruct old x +assigned +-- a value the target does not keep is released at its assignment +construct k +construct l +set k +destruct k +set l +destruct l +assigned +construct old b +construct first +construct second +destruct first +destruct old b +assigned +destruct second +-- the same with a destructor that throws +boom +int(0) +-- two targets that are references to each other +boom +int(0) +-- a variable that is a reference to a typed property +construct coerced +destruct coerced +set y +assigned +-- pending values when a target expression throws +construct v1 +construct v2 +construct v3 +destruct v1 +destruct v2 +destruct v3 +boom +-- a variable is copied before any assignment +construct o +construct new o +assigned +string(5) "new o" +string(1) "o" +destruct new o +destruct o +-- exception in a later value +construct first +construct second +destruct first +destruct second +boom +-- exception in an assignment +construct after the throw +destruct after the throw +TypeError +int(1) +-- a destructor that throws when a target drops the value +construct a +construct b +set a +destruct a throws +destruct b +boom a +done diff --git a/Zend/tests/list/list_from_array_literal_undefined.phpt b/Zend/tests/list/list_from_array_literal_undefined.phpt new file mode 100644 index 000000000000..9898e37f7780 --- /dev/null +++ b/Zend/tests/list/list_from_array_literal_undefined.phpt @@ -0,0 +1,31 @@ +--TEST-- +List assignment from an array literal: undefined variables +--FILE-- + +--EXPECTF-- + +Warning: Undefined variable $u0 in %s on line 4 + +Warning: Undefined variable $u1 in %s on line 4 +NULL +NULL + +Warning: Undefined variable $u2 in %s on line 9 + +Warning: Undefined variable $u3 in %s on line 10 +NULL +NULL diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ffd40f06014d..d2986d15e1d1 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3516,6 +3516,68 @@ static void zend_compile_expr_with_potential_assign_to_self( } } +/* Flat unkeyed list and literal without references or spread, one value per target */ +static bool zend_list_assign_from_array_applies(zend_ast *var_ast, zend_ast *expr_ast) /* {{{ */ +{ + if (expr_ast->kind != ZEND_AST_ARRAY) { + return false; + } + + const zend_ast_list *targets = zend_ast_get_list(var_ast); + const zend_ast_list *values = zend_ast_get_list(expr_ast); + if (targets->children == 0 || values->children != targets->children) { + return false; + } + + for (uint32_t i = 0; i < targets->children; i++) { + const zend_ast *target_ast = targets->child[i]; + const zend_ast *value_ast = values->child[i]; + if (target_ast == NULL || target_ast->kind != ZEND_AST_ARRAY_ELEM || target_ast->child[1] != NULL || target_ast->attr + || target_ast->child[0]->kind == ZEND_AST_ARRAY) { + return false; + } + if (value_ast == NULL || value_ast->kind != ZEND_AST_ARRAY_ELEM || value_ast->child[1] != NULL || value_ast->attr) { + return false; + } + } + return true; +} +/* }}} */ + +/* [$a, $b] = [$b, $a]; with an unused result: assigns the values directly, without the array. + * The values are compiled in order and a variable is copied before any assignment; each target + * then takes its value */ +static bool zend_try_compile_list_assign_from_array(zend_ast *var_ast, zend_ast *expr_ast) /* {{{ */ +{ + if (!zend_list_assign_from_array_applies(var_ast, expr_ast)) { + return false; + } + + const zend_ast_list *targets = zend_ast_get_list(var_ast); + const zend_ast_list *values = zend_ast_get_list(expr_ast); + uint32_t count = values->children; + znode *value_nodes = safe_emalloc(count, sizeof(znode), 0); + + for (uint32_t i = 0; i < count; i++) { + znode *value = &value_nodes[i]; + zend_compile_expr(value, values->child[i]->child[0]); + if (value->op_type == IS_CV) { + zend_emit_op_tmp(value, ZEND_QM_ASSIGN, value, NULL); + } + } + + for (uint32_t i = 0; i < count; i++) { + zend_ast *target_ast = targets->child[i]->child[0]; + + zend_verify_list_assign_target(target_ast, var_ast->attr); + zend_emit_assign_znode(target_ast, &value_nodes[i]); + } + + efree(value_nodes); + return true; +} +/* }}} */ + static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_t type) /* {{{ */ { zend_ast *var_ast = ast->child[0]; @@ -3592,6 +3654,10 @@ static void zend_compile_assign(znode *result, zend_ast *ast, bool stmt, uint32_ * self-assignments, this forces the RHS to evaluate first. */ zend_emit_op(&expr_node, ZEND_MAKE_REF, &expr_node, NULL); } else { + if (stmt && zend_try_compile_list_assign_from_array(var_ast, expr_ast)) { + result->op_type = IS_UNUSED; + return; + } if (expr_ast->kind == ZEND_AST_VAR) { /* list($a, $b) = $a should evaluate the right $a first */ znode cv_node; @@ -6579,7 +6645,8 @@ static void zend_compile_do_while(const zend_ast *ast) /* {{{ */ } /* }}} */ -static void zend_compile_for_expr_list(znode *result, zend_ast *ast) /* {{{ */ +/* An assignment whose result is unused is compiled as a statement */ +static void zend_compile_for_expr_list(znode *result, zend_ast *ast, bool last_result_used) /* {{{ */ { const zend_ast_list *list; uint32_t i; @@ -6600,6 +6667,8 @@ static void zend_compile_for_expr_list(znode *result, zend_ast *ast) /* {{{ */ zend_compile_void_cast(NULL, expr_ast); result->op_type = IS_CONST; ZVAL_NULL(&result->u.constant); + } else if (expr_ast->kind == ZEND_AST_ASSIGN && (i + 1 < list->children || !last_result_used)) { + zend_compile_assign(result, expr_ast, /* stmt */ true, BP_VAR_R); } else { zend_compile_expr(result, expr_ast); } @@ -6617,7 +6686,7 @@ static void zend_compile_for(const zend_ast *ast) /* {{{ */ znode result; uint32_t opnum_start, opnum_jmp, opnum_loop; - zend_compile_for_expr_list(&result, init_ast); + zend_compile_for_expr_list(&result, init_ast, false); zend_do_free(&result); opnum_jmp = zend_emit_jump(0); @@ -6628,11 +6697,11 @@ static void zend_compile_for(const zend_ast *ast) /* {{{ */ zend_compile_stmt(stmt_ast); opnum_loop = get_next_op_number(); - zend_compile_for_expr_list(&result, loop_ast); + zend_compile_for_expr_list(&result, loop_ast, false); zend_do_free(&result); zend_update_jump_target_to_next(opnum_jmp); - zend_compile_for_expr_list(&result, cond_ast); + zend_compile_for_expr_list(&result, cond_ast, true); zend_do_extended_stmt(NULL); zend_emit_cond_jump(ZEND_JMPNZ, &result, opnum_start); diff --git a/ext/opcache/tests/opt/list_assign_from_array_literal.phpt b/ext/opcache/tests/opt/list_assign_from_array_literal.phpt new file mode 100644 index 000000000000..5a586c61874f --- /dev/null +++ b/ext/opcache/tests/opt/list_assign_from_array_literal.phpt @@ -0,0 +1,93 @@ +--TEST-- +List assignment from an array literal compiles without the array +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=0 +opcache.opt_debug_level=0x20000 +opcache.preload= +zend_test.observer.enabled=0 +--EXTENSIONS-- +opcache +--FILE-- + 0; $n--, [$a, $b] = [$b, $a + $b]); + return $a; +} +function elements($p) { + [$p[0], $p[1]] = [$p[1], $p[0]]; + return $p; +} +function twice($x) { + [$x, $x] = [$x, 2]; + return $x; +} +?> +--EXPECTF-- + +$_main: + ; (lines=1, args=0, vars=0, tmps=0) + ; (after optimizer) + ; %slist_assign_from_array_literal.php:1-19 +0000 RETURN int(1) + +swap: + ; (lines=8, args=2, vars=2, tmps=4) + ; (after optimizer) + ; %slist_assign_from_array_literal.php:2-5 +0000 CV0($a) = RECV 1 +0001 CV1($b) = RECV 2 +0002 T2 = QM_ASSIGN CV1($b) +0003 T3 = QM_ASSIGN CV0($a) +0004 ASSIGN CV0($a) T2 +0005 ASSIGN CV1($b) T3 +0006 RETURN CV0($a) +0007 RETURN null + +fibonacci: + ; (lines=13, args=1, vars=3, tmps=8) + ; (after optimizer) + ; %slist_assign_from_array_literal.php:6-9 +0000 CV0($n) = RECV 1 +0001 ASSIGN CV1($a) int(0) +0002 ASSIGN CV2($b) int(1) +0003 JMP 0009 +0004 PRE_DEC CV0($n) +0005 T6 = QM_ASSIGN CV2($b) +0006 T7 = ADD CV1($a) CV2($b) +0007 ASSIGN CV1($a) T6 +0008 ASSIGN CV2($b) T7 +0009 T10 = IS_SMALLER int(0) CV0($n) +0010 JMPNZ T10 0004 +0011 RETURN CV1($a) +0012 RETURN null + +elements: + ; (lines=9, args=1, vars=1, tmps=4) + ; (after optimizer) + ; %slist_assign_from_array_literal.php:10-13 +0000 CV0($p) = RECV 1 +0001 T1 = FETCH_DIM_R CV0($p) int(1) +0002 T2 = FETCH_DIM_R CV0($p) int(0) +0003 ASSIGN_DIM CV0($p) int(0) +0004 OP_DATA T1 +0005 ASSIGN_DIM CV0($p) int(1) +0006 OP_DATA T2 +0007 RETURN CV0($p) +0008 RETURN null + +twice: + ; (lines=6, args=1, vars=1, tmps=3) + ; (after optimizer) + ; %slist_assign_from_array_literal.php:14-17 +0000 CV0($x) = RECV 1 +0001 T1 = QM_ASSIGN CV0($x) +0002 ASSIGN CV0($x) T1 +0003 ASSIGN CV0($x) int(2) +0004 RETURN CV0($x) +0005 RETURN null \ No newline at end of file