Skip to content

PHP array_shift(): Optimize tail move on hole-free packed arrays - #23517

Draft
mehmetcansahin wants to merge 1 commit into
php:masterfrom
mehmetcansahin:array-shift-loop
Draft

PHP array_shift(): Optimize tail move on hole-free packed arrays#23517
mehmetcansahin wants to merge 1 commit into
php:masterfrom
mehmetcansahin:array-shift-loop

Conversation

@mehmetcansahin

@mehmetcansahin mehmetcansahin commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

For a packed array without holes, removing the first element leaves a contiguous tail. When no iterator is active, array_shift() now moves that tail with a single memmove() instead of testing every slot for IS_UNDEF; a one element tail is moved directly to avoid the call overhead. Packed arrays with holes, arrays with active iterators, and hash arrays keep the existing paths.

Benchmarks

Draining a packed int array ($a = $base; while ($a) array_shift($a);):

n repetitions base this PR speedup
100 20,000 160.5 ms 58.7 ms 2.7×
1,000 2,000 1,075.6 ms 249.9 ms 4.3×
10,000 200 10,726.5 ms 2,602.4 ms 4.1×

Two-element regression check ($a = [0, 1]; array_shift($a); x 100M, exercises the direct-copy specialization): 3.528s → 3.438 s, no tiny-array slowdown observed.

Benchmark scripts

Draining benchmark:

<?php
foreach ([100, 1000, 10000] as $n) {
    $reps = intdiv(2000000, $n);
    $base = range(0, $n - 1);
    $best = PHP_FLOAT_MAX;
    for ($run = 0; $run < 3; $run++) {
        $t = hrtime(true);
        for ($r = 0; $r < $reps; $r++) {
            $a = $base;
            while ($a) array_shift($a);
        }
        $best = min($best, (hrtime(true) - $t) / 1e6);
    }
    printf("n=%-6d drain x%-5d best: %8.1f ms\n", $n, $reps, $best);
}

Two-element regression check:

<?php
$best = PHP_FLOAT_MAX;
for ($run = 0; $run < 3; $run++) {
    $t = hrtime(true);
    for ($i = 0; $i < 100000000; $i++) {
        $a = [0, 1];
        array_shift($a);
    }
    $best = min($best, (hrtime(true) - $t) / 1e9);
}
printf("fresh [0,1] x100M best: %.3f s\n", $best);

When a packed array has no holes and no active iterators, the slot
vacated by the shifted element is the only hole, so the tail can be
moved down with a single memmove instead of testing every slot for
IS_UNDEF and comparing source and destination indexes. A tail of a
single element is moved directly instead, avoiding the memmove call
overhead on tiny arrays.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant