Skip to content

array_unshift(): Shift packed arrays in place - #24475

Open
mehmetcansahin wants to merge 1 commit into
php:masterfrom
mehmetcansahin:array-unshift-packed-move
Open

mehmetcansahin wants to merge 1 commit into
php:masterfrom
mehmetcansahin:array-unshift-packed-move

Conversation

@mehmetcansahin

Copy link
Copy Markdown
Contributor

This PR speeds up array_unshift() on packed arrays by shifting values in place with memmove() instead of allocating and rebuilding the table. The fast path applies when there are no holes or active iterators and rebuilding would use the same table size. Other paths, including table growth and shrinking, are unchanged.

Benchmarks

Prepending one value on Apple M1, release/NTS build (php -n), using the median of seven runs per build, alternating base and PR:

input base ns/op this PR ns/op change
packed, n=7 45.4 35.1 -22.8%
packed, n=1,000 496.9 245.1 -50.7%
packed, n=100,000 158,820.8 29,289.2 -81.6%
copy of packed, n=1,000 1,640.6 1,377.7 -16.0%
string keys, n=1,000 (old path) 10,220.4 10,274.7 +0.5%

Tested with the array suite on release and debug ASan/UBSan builds.

Benchmark script and methodology

Base 1c9242e7fec, this PR a667208a4da; Apple clang 21, -O2 -DNDEBUG, OPcache off. Each process calibrates to at least 60 ms and reports the lowest average time over five batches. Packed cases include the array_pop() calls that keep the size constant; copy cases include copy-on-write separation. Negative change means faster.

Run as php -n bench.php '<case>' 60000000.

<?php
function steady(int $n, int $k): Closure {
    return function (int $reps) use ($n, $k): int {
        $a = range(1, $n);
        $vals = range(-$k, -1);
        for ($r = 0; $r < $reps; $r++) {
            array_unshift($a, ...$vals);
            for ($j = 0; $j < $k; $j++) array_pop($a);
        }
        return count($a);
    };
}
function copyThen(array $src): Closure {
    return function (int $reps) use ($src): int {
        for ($r = 0; $r < $reps; $r++) {
            $b = $src;
            array_unshift($b, 0);
        }
        return count($b);
    };
}
$hash = [];
for ($i = 0; $i < 1000; $i++) $hash["k$i"] = $i;

$cases = [
    'packed, 7 elements, 1 value'             => steady(7, 1),
    'packed, 1,000 elements, 1 value'         => steady(1000, 1),
    'packed, 100,000 elements, 1 value'       => steady(100000, 1),
    'copy of packed, 1,000 elements, 1 value' => copyThen(range(1, 1000)),
    'copy of string keys, 1,000 elements, 1 value' => copyThen($hash),
];

$run = $cases[$argv[1]];
$target = (int) $argv[2];
$reps = 1;
do {
    $t = hrtime(true);
    $run($reps);
    $d = hrtime(true) - $t;
    if ($d >= $target) break;
    $reps *= 2;
} while (true);
$best = PHP_FLOAT_MAX;
for ($i = 0; $i < 5; $i++) {
    $t = hrtime(true);
    $run($reps);
    $best = min($best, (hrtime(true) - $t) / $reps);
}
echo json_encode(['ns' => $best]), "\n";

Packed arrays without holes and without iterators are shifted in place
when rebuilding would allocate a table of the same size. Otherwise the
array is still rebuilt, so unused slots are released.
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.

2 participants