[php-src] PR #24157: array_push(): Append to packed arrays in bulk

From: 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.

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