Skip to content

Repository files navigation

alpha — _alpha parallel variants of PHP's array functions

alpha is a standalone PHP extension providing 39 _alpha sibling functions of the standard library's array/search/sort functions. Each _alpha function is a parallel reimplementation of its original (array_slicearray_slice_alpha, ksortksort_alpha, …) that adds a fast path for the common shapes (dense packed arrays, contiguous key lists, SIMD-fillable progressions) and falls back to a byte-for-byte copy of the original's code everywhere else.

The function bodies are vendored verbatim from ext/standard/array.c and ext/standard/type.c (PHP 8.6); the only changes are the extension's own module globals (ALPHAG(...)) and small localized helpers. The originals in ext/standard are left completely untouched.

The contract (enforced by bench/alpha_guard.sh): every _alpha function must return exactly what the original returns and behave identically — it must be faster-or-equal on its fast-path shapes, and equal-within-noise where it falls back to the baseline.

Requirements

  • Builds with phpize against the host PHP — verified with PHP 8.4.24 (NTS) and the PHP 8.6.0-dev source tree the function bodies are vendored from
  • NTS and ZTS builds are both supported
  • No external dependencies; the extension depends on the standard extension being available (it is always loaded in PHP CLI/FPM)

Installation

Linux / macOS (phpize)

git clone <this repo> && cd array_alpha
phpize
./configure --enable-alpha
make
sudo make install

Then enable it in your php.ini:

extension=alpha.so

(On macOS the shared object is alpha.dylib.)

Windows

config.w32 is provided; build with the standard Windows PHP build system:

configure.js --enable-alpha
nmake

and copy the resulting php_alpha.dll into your extension dir.

Via Composer (php-ext)

The repo ships a composer.json of type php-ext (array-alpha/array-alpha), so it can also be built through the php-ext tooling:

{
    "require": {
        "array-alpha/array-alpha": "^0.1"
    }
}

Usage

Load the extension and call the _alpha variants wherever you would call the original. Signatures, return values, warnings and exceptions are identical — swapping the call site is the only change:

var_dump(array_sum_alpha([1, 2, 3.5]));          // float(6.5)
var_dump(array_fill_alpha(-2, 3, 'x'));          // ['x', 'x', 'x'] keyed -2..0
var_dump(range_alpha(0, 1000000));               // packed int range, SIMD-filled
var_dump(array_is_list_alpha([1, 2, 3]));        // bool(true)

$scores = [3 => 'c', 1 => 'a', 2 => 'b'];
asort_alpha($scores);                            // in-place, like asort()

Where the original would return true (in-place sorts), the _alpha variant returns true; where the original returns int|string|false (array_search), so does array_search_alpha.

A subset of the pure-array functions supports compile-time evaluation by OPcache (same as the built-ins): array_slice_alpha, array_count_values_alpha, array_column_alpha, array_reverse_alpha, array_flip_alpha, array_key_exists_alpha, array_combine_alpha, array_is_list_alpha.

Functions provided

Original _alpha variant
Sorting (in-place, return true)
krsort(), ksort(), natsort(), natcasesort(), arsort(), asort() krsort_alpha(), ksort_alpha(), natsort_alpha(), natcasesort_alpha(), arsort_alpha(), asort_alpha()
array_multisort() array_multisort_alpha()
Search
in_array(), array_search(), array_key_exists() in_array_alpha(), array_search_alpha(), array_key_exists_alpha()
Build / transform
array_fill(), array_fill_keys(), range() array_fill_alpha(), array_fill_keys_alpha(), range_alpha()
array_merge(), array_merge_recursive() array_merge_alpha(), array_merge_recursive_alpha()
array_replace(), array_replace_recursive() array_replace_alpha(), array_replace_recursive_alpha()
array_combine(), array_reverse(), array_flip() array_combine_alpha(), array_reverse_alpha(), array_flip_alpha()
array_chunk(), array_slice(), array_splice() array_chunk_alpha(), array_slice_alpha(), array_splice_alpha()
array_keys(), array_column() array_keys_alpha(), array_column_alpha()
Aggregates
array_sum(), array_product(), array_count_values(), array_unique() array_sum_alpha(), array_product_alpha(), array_count_values_alpha(), array_unique_alpha()
Set operations
array_intersect(), array_intersect_key(), array_intersect_assoc() array_intersect_alpha(), array_intersect_key_alpha(), array_intersect_assoc_alpha()
array_diff(), array_diff_key(), array_diff_assoc(), array_udiff() array_diff_alpha(), array_diff_key_alpha(), array_diff_assoc_alpha(), array_udiff_alpha()
Stack operations
array_shift(), array_unshift() array_shift_alpha(), array_unshift_alpha()
Predicates
array_is_list() array_is_list_alpha()

Performance (Raspberry Pi 5)

Full sweep of all 39 microbenchmarks on a Raspberry Pi 5 (Cortex-A76, aarch64), run via ./bench/alpha_guard.sh — 3 runs per bench, each bench using the order-alternated minimum-of-8 harness in bench/bench_alpha_lib.php, best ratio kept. Environment: PHP 8.4.24 CLI (NTS release build, Debian packaging), the alpha extension compiled against that PHP's API and loaded via -d extension=modules/alpha.so, no OPcache. The sweep parsed 331 shapes (302 measured, 29 trivially skipped by the guard).

ratio = original ÷ alpha (higher is better for _alpha). Per function: the best measured shape, and the worst measured shape (typically an intentional fallback, expected ≈ 1.0x).

Function Best measured shape Best ratio Worst measured shape (expected parity / fallback)
array_intersect_alpha packed int, three arrays (33% overlap) 19.4x packed string values 100% overlap 7.8x
array_diff_assoc_alpha int N=20k identical 12.2x cross-type coercion fallback 1.00x
array_is_list_alpha packed hole detection, N=10k 10.3x assoc N=1k 1.00x
array_intersect_assoc_alpha int N=20k shifted 10.2x cross-type coercion fallback 0.94x
array_chunk_alpha packed dense, one big chunk 4.2x hash input, no preserve fallback 0.82x
array_splice_alpha packed 10k, remove last 1 (tail) 3.8x assoc str-keys slow path 1.01x
array_merge_recursive_alpha 1k + 20×1k packed sources, in-place 3.5x hash int-keys fallback 0.99x
array_unshift_alpha list (10000, int) 3.0x empty (prepend 1) 0.98x
array_keys_alpha packed keys + search_value (strict) 2.9x packed contiguous, no search_value 1.00x
array_fill_alpha hash path (−5, 10000, int) 2.7x empty (0, 0, int) 1.00x
array_combine_alpha packed contiguous long keys 2.3x packed shuffled long keys 0.93x
array_fill_keys_alpha contiguous list (10000, int) 2.2x numstr keys 0.85x
array_sum_alpha packed longs, no overflow 2.1x packed doubles 1.70x
array_product_alpha packed range(1,10000), overflow early 1.9x packed longs, no overflow 1.27x
krsort_alpha packed int keys 1k 1.9x string keys fallback 0.97x
asort_alpha packed long 10k 1.9x packed doubles 10k 1.01x
array_shift_alpha list (10000, int) 1.9x list (8, int) tiny 0.99x
array_unique_alpha dup-string 2x (1000) 1.8x packed-int, SORT_REGULAR flag 0.85x
array_replace_alpha packed first + 3 sources (in-place) 1.7x hash fallback 1.01x
arsort_alpha packed long 1k 1.7x packed doubles 10k 1.04x
array_diff_key_alpha str N=20k identical keys 1.7x int disjoint keys 1.25x
array_search_alpha packed long needle present (loose) 1.6x string needle full scan 1.17x
in_array_alpha packed long needle present (loose) 1.6x string needle full scan 1.12x
array_replace_recursive_alpha packed 1k + 20×1k (append-heavy) 1.6x hash control 1.01x
range_alpha int step-1, N=100k 1.5x tiny range(0,3) 1.00x
array_count_values_alpha packed mixed long/string 1.3x packed strings 1.01x
ksort_alpha packed int keys 10k 1.3x mixed keys fallback 0.98x
array_slice_alpha list 100k, !preserve, mid-slice 1.3x list 10k, preserve mid-slice 0.82x
array_diff_alpha packed int, 100% overlap 1.2x packed first + hash other fallback 0.92x
array_intersect_key_alpha int N=20k shifted 1.2x str identical keys fallback 1.03x
natcasesort_alpha long strings 10k 1.2x long 10k fallback 0.94x
array_reverse_alpha mixed int keys (10000) preserve 1.1x list 1k !preserve 0.89x
array_column_alpha hash-keyed outer input 1.1x packed rows, int col + int index 0.92x
array_flip_alpha sparse (1000, int) 1.1x numstr (1000, str) 0.90x
array_merge_alpha hash 10k str-keys + small 1.1x 3× packed 1k in-place 1.00x
natsort_alpha numstr 10k 1.0x long 10k fallback 0.89x
array_multisort_alpha packed 1k strings ×2, SORT_NATURAL 1.0x packed 1k strings ×2, SORT_STRING 0.88x
array_key_exists_alpha packed int miss (99999) 1.0x packed int out-of-range 1.00x

(array_udiff_alpha's benchmark reports callback-count reduction instead of wall time: 1.0–3.8x fewer user-land comparison callbacks, result=ok on all shapes.)

Two caveats specific to this sweep:

  • The four benches whose harness does not fit the guard's per-run 600 s timeout on this host — arsort, asort, krsort, natcasesort — were killed mid-run each time; the ratios above come from the shapes that complete within the timeout. Missing from those counts: arsort/asort (string 10k, mixed long/string 10k), krsort (mixed keys 10k), natcasesort (double 10k, mixed 10k).
  • The set-operation benches measure against PHP 8.4's baselines, which are considerably slower than 8.6's on array_intersect/array_diff* (the vendored 8.6 algorithm in _alpha is unchanged) — this is why those ratios are far larger than in earlier 8.6-based sweeps.

Corroborating results from this sweep, matching the per-shape medians in the bench-script headers:

  • range_alpha(), integer step-1 range: 1.51x at N = 100k and 1.15x at N = 1M — the SIMD (NEON, 2 zvals/iter on aarch64; AVX2, 4 zvals/iter on x86_64) strided packed-zval fill. Decreasing ranges measure 1.09–1.14x; smaller/strided shapes 1.0–1.2x. Float, char and single-element ranges are parity by construction (identical code path, 1.00–1.15x measured).
  • array_fill_alpha(), hash path (negative start_index): 2.0 – 2.7x faster — the per-element out-of-line hash inserts are replaced by a tight inlined bucket+hash-slot fill loop (packed/positive-start_index shapes: 1.4 – 1.8x).

Known slow shapes on this build

The guard's contract is faster-or-equal, always. In this sweep 3 of 302 measured shapes came in below the 0.85x noise floor, all of them fallback/parity shapes expected to sit at ≈ 1.0x:

  • array_chunk_alpha — packed-with-holes (0.83x) and hash (0.82x) inputs, no preserve: both are intentional fallbacks running the byte-for-byte baseline code, so any difference here is dispatch/binary-layout, not algorithm.
  • array_slice_alpha — list (10000, int) preserve, mid-slice (0.82x): preserve_keys re-keys every element; the _alpha variant only wins on slices where the packed fast path applies.

Near-floor shapes (0.85 – 0.94x, inside or just above this host's binary-layout noise band, identical code paths measure 0.89 – 1.10x here): array_fill_keys (numstr keys), array_unique (SORT_REGULAR flag path), array_multisort (SORT_STRING), array_reverse (list/assoc), array_column (int col + int index), array_combine (shuffled long keys), natsort/natcasesort (long-string fallback). If your workload sits on one of these shapes, measure before swapping the call site — and see bench/alpha_guard.sh for the regression harness.

Methodology and caveats

  • The Raspberry Pi 5 is thermally constrained: a throttling event can distort a single measurement (a genuinely 1.35x-faster shape has been observed reading 0.73x in one throttled run). The harness therefore alternates arm order per repeat and reports the minimum of 8 repeats per arm (the coolest, least throttled pass), and the regression guard re-runs each bench and keeps the best ratio of N runs.
  • Identical-code parity shapes measure ~0.90 – 1.12x on this host: the ~0.90 lower bound is a binary-layout effect (the larger _alpha function body can slightly degrade codegen/alignment of a loop that is byte-for-byte identical to the baseline's).
  • These numbers are hardware- and compiler-specific. Re-run the benchmarks on your target hardware (see below) before making a call-site swap.

Testing

The extension ships 316 .phpt tests (tests/, one directory per function), covering packed/hash shapes, holes, references, overflow, error paths, and equivalence tests that assert _alpha output equals the original's output. Run them with:

make test

or against a specific binary/extension:

php run-tests.php -d extension=/path/to/alpha.so tests/

The bench/compare_*_alpha.php scripts double as live correctness/memory comparisons (they assert the _alpha result equals the original's result for many shapes); they are intended for use with a debug + ASan build.

Benchmarking / regression guard

Every function has a microbenchmark in bench/. The guard runs all of them, classifies each shape, and fails if any measured shape shows _alpha slower than its original beyond the noise floor:

./bench/alpha_guard.sh
# useful env vars:
#   ALPHA_GUARD_PHP=path/to/php-cli     (release build recommended)
#   ALPHA_GUARD_EXT=path/to/alpha.so    (when the binary lacks alpha built in)
#   ALPHA_GUARD_RUNS=3                  (best-of-N per shape; 1 on stable hosts)
#   ALPHA_GUARD_MIN_RATIO=0.85          (0.95 on stable hosts)
#   ALPHA_GUARD_ONLY=array_merge        (run one bench)

Each bench script prints one line per shape: <label> iters=<N> base=<ns> ns alpha=<ns> ns ratio=<x>x speedup=<%> (ratio > 1.0_alpha faster).

License

BSD-3-Clause — see LICENSE. The function bodies are vendored from PHP's ext/standard (© The PHP Group and Contributors).

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages