[php-src] PR #24157: array_push(): Append to packed arrays in bulk
| From: | mehmetcansahin | Date: | Tue, 06 Oct 2026 08:03:16 +0000 |
| Subject: | [php-src] PR #24157: array_push(): Append to packed arrays in bulk | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39197@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24157
Author: mehmetcansahin
array_push() used to call zend_hash_next_index_insert() for each value.
When the array is packed (or uninitialized, with 2+ values) and its next free index is right after
the last used slot, this makes room once when needed and writes the values directly with
ZEND_HASH_FILL_*. This mainly helps array_push($a, ...$values).
The old loop is still used for: hash arrays; packed arrays whose next free index is past the last
used slot (e.g. after unset() of the last element); packed arrays with holes that would
need to grow (growth may turn them into a hash); a single value pushed onto an uninitialized empty
array.
Keys, the next free index, the internal pointer and table-growth rules stay the same.
Benchmark: [script](https://gist.github.com/mehmetcansahin/b87c7c88d674148ff196db69776c7f5d).
Release build (-O2 -DNDEBUG, NTS), OPcache/JIT off (php -n),
20,000–10,000,000 calls per repetition depending on the case, 15 alternating base/PR pairs (best
of 5 repetitions per run), base 421347dff42. Apple M1 (macOS) and Xeon Platinum 8370C
(Linux VM, pinned to one CPU).
Times are medians in ns/op. Change = PR median / base median - 1, calculated before rounding.
Negative means faster.
| Scenario | M1 base ns/op | M1 PR ns/op | Change | Xeon base ns/op | Xeon PR ns/op | Change |
|---|---:|---:|---:|---:|---:|---:|
| packed, 1 value | 15.03 | 14.90 | -0.8% | 17.23 | 17.00 | -1.3% |
| packed, 3 values | 25.08 | 20.53 | -18.2% | 29.54 | 24.76 | -16.2% |
| packed, 16 values via spread | 145.99 | 99.83 | -31.6% | 131.77 | 90.08 | -31.6% |
| empty, 1000 values via spread | 8918.89 | 5238.79 | -41.3% | 8263.94 | 4392.19 | -46.9% |
| packed, 1 string | 18.64 | 17.03 | -8.7% | 19.90 | 19.25 | -3.3% |
| empty, 1 value | 36.28 | 37.88 | +4.4% | 34.98 | 36.41 | +4.1% |
| empty, 2 values | 41.56 | 42.38 | +2.0% | 41.36 | 37.98 | -8.2% |
| packed with holes, 1 value | 18.64 | 17.07 | -8.4% | 18.02 | 17.33 | -3.8% |
| hash, 1 value | 20.38 | 20.30 | -0.4% | 21.62 | 21.28 | -1.6% |
| hash, 3 values | 39.87 | 39.57 | -0.8% | 41.84 | 41.47 | -0.9% |
Hash rows use the unchanged insertion loop; their measured changes are within ±2%. The empty,
1 value case uses the old loop after the extra checks and measured about 1.5 ns/call slower;
its timing includes $a = [] and freeing the previous array.
empty, 2 values is mixed (+2.0% M1, −8.2% Xeon): one possible explanation is that
saving the second insert may be offset by the separate zend_hash_real_init_packed()
call.
The bench-common.php verify outputs match for base and PR on both machines.