[php-src] PR #24180: Zend: Compile a list assignment from an array literal without the array

From: Date: Wed, 07 Oct 2026 19:28:41 +0000
Subject: [php-src] PR #24180: Zend: Compile a list assignment from an array literal without the array
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-39240@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/24180 Author: ArtUkrainskiy Closes GH-23048. [$a, $b] = [$b, $a]; builds an array, fetches both elements back out of it and frees it. When the result is unused and the list is flat, unkeyed, without references or spread, with one value per target, this assigns the values directly: ``` T2 = QM_ASSIGN CV1($b) T3 = QM_ASSIGN CV0($a) T4 = COPY_TMP T2 ASSIGN CV0($a) T4 T6 = COPY_TMP T3 ASSIGN CV1($b) T6 FREE T2 FREE T3 ``` That is the shape from the issue plus a copy per target. Without the copies the four opcodes are 467 instructions instead of 523, but the array held every value until after the last assignment, and dropping that is observable: a setter that discards the value, two targets that are references to each other, and a destructor that throws — on master [$a, $a, $b] = [new Boom, new stdClass, 42] still assigns $b, without the copies the exception cuts the statement short. I tried keeping the direct assignment for plain variables, but whether a variable still holds its value at the end of the statement is a runtime question (references, user code in a later target's expression), so every target gets a copy and the values are freed where the array was. With that I found no remaining difference, destruction order under exceptions included. The expression lists of a for loop get the same treatment, since their results are unused too. Nested lists stay as they are: the inner list needs an owner whose destruction order matches the old fetches, and a COPY_TMP freed later can't be one — its live range is computed for the ?? pattern. Skipped or extra values stay too: the copy that would keep an untaken value alive is QM_ASSIGN + FREE, which the block pass turns into CHECK_VAR. Release builds, perf stat instructions per statement: | statement | instructions | |---|---| | [$a, $b] = [$b, $a] | 922 → 523 | | [$a, $b, $c] = [$b, $c, $a] | 1,119 → 629 | | ten Fibonacci steps [$a, $b] = [$b, ($a + $b) % M] | 6,114 → 2,064 | | [$p[0], $p[1]] = [$p[1], $p[0]] | 1,290 → 879 | | swap through a temporary variable, for reference | 493 | A loop of 300,000 swaps with opcache: 5.1 → 1.1 ms, with the tracing JIT 3.8 → 0.5 ms. In two large vendor trees (327,462 op_arrays) 56 statements change and nothing else does, so this is for the odd hot swap, not for applications. Verified against master: 97 behaviour cases and a sweep of 3,376 generated programs with printing destructors and setters, identical output on release and debug builds, with opcache and both JITs. Test expectations are generated on master; the lifetime test also runs with opcache.optimization_level=-1. Details and reproduction: https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/list-assign-from-array-literal Possible follow-ups: keyed lists with matching constant keys, and an optimizer rule dropping COPY_TMP/FREE for values known not to be refcounted, which would give typed swaps the four-opcode shape.

« previous php.git-pulls (#39240) next »