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_slice → array_slice_alpha,
ksort → ksort_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.
- Builds with
phpizeagainst 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
standardextension being available (it is always loaded in PHP CLI/FPM)
git clone <this repo> && cd array_alpha
phpize
./configure --enable-alpha
make
sudo make installThen enable it in your php.ini:
extension=alpha.so(On macOS the shared object is alpha.dylib.)
config.w32 is provided; build with the standard Windows PHP build system:
configure.js --enable-alpha
nmakeand copy the resulting php_alpha.dll into your extension dir.
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"
}
}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.
| 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() |
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_alphais 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 (negativestart_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_indexshapes: 1.4 – 1.8x).
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_keysre-keys every element; the_alphavariant 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.
- 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
_alphafunction 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.
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 testor 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.
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).
BSD-3-Clause — see LICENSE. The function bodies are vendored from
PHP's ext/standard (© The PHP Group and Contributors).