Benchmarks and verification
Distribution performance and regression preservation
The distribution benchmark compares a PHP scalar loop, one native PHP batch call, an internal Rust loop, vectorized R and SciPy on identical saved temporary inputs. It covers 1,000, 100,000 and one million values for normal CDF/SF, Student-t quantile, binomial/Poisson log-PMF, mean/variance, beta quantile and chi-square CDF with df one million. Raw results retain every measurement, numerical summary, input hash, runtime version, thread setting and peak resident memory.
Numerical comparisons before timings
All input cases are verified across implementations before any timing phase begins. Seventeen evenly spaced output values plus min/max use the distribution fixture tolerance: 1e-300 + 1e-12*abs(reference), or 1e-15 + 1e-12*abs(reference) for quantiles. Sum and sum-of-squares consume all outputs, with tolerance 1e-9 + 1e-10*abs(reference) to allow different accumulation orders. Mean/variance outputs are checked directly. R is the benchmark reference; the separate R/SciPy/mpmath record adjudicates reference differences and tests extreme/boundary values more broadly.
Native comparisons must pass. SciPy peer discrepancies are retained and labeled; they do not silently relax the native gate. Every timed output summary is checked again. This benchmark does not compare every individual element of the large vectors; the complete-vector aggregates and sampled values complement the strict small reference/identity suite.
Recorded: 2026-10-12T15:18:59.861643+00:00; Apple M5 Ultra, macOS-27.0.1-arm64-arm-64bit-Mach-O. 27 input cases; all native/R comparisons passed. SciPy exceeded the same tolerance for: chisquare_cdf (1,000), chisquare_cdf (100,000), chisquare_cdf (1,000,000).
Median milliseconds per full call, 5 measurements after one warm-up:
| Operation | Values | PHP scalar loop | PHP batch/native | Rust loop | R | SciPy |
|---|---|---|---|---|---|---|
| normal_cdf | 1,000 | 0.04073 | 0.02235 | 0.01054 | 0.02 | 0.01851 |
| normal_cdf | 100,000 | 4.093 | 2.23 | 1.374 | 2 | 1.227 |
| normal_cdf | 1,000,000 | 41.28 | 23.62 | 14.29 | 17 | 12.1 |
| normal_sf | 1,000 | 0.04412 | 0.02112 | 0.01159 | 0.02 | 0.0193 |
| normal_sf | 100,000 | 4.406 | 2.153 | 1.238 | 2 | 0.9886 |
| normal_sf | 1,000,000 | 44.06 | 21.05 | 14.5 | 16 | 9.896 |
| t_quantile | 1,000 | 0.5982 | 0.5775 | 0.5745 | 0.23 | 0.1402 |
| t_quantile | 100,000 | 59.79 | 57.86 | 56.78 | 23 | 12.53 |
| t_quantile | 1,000,000 | 601.9 | 580 | 569.8 | 222 | 122.2 |
| binomial_logpmf | 1,000 | 0.07055 | 0.03282 | 0.02151 | 0.03 | 0.03847 |
| binomial_logpmf | 100,000 | 6.817 | 3.474 | 2.65 | 3 | 2.488 |
| binomial_logpmf | 1,000,000 | 66.09 | 35.04 | 26.67 | 31 | 24.8 |
| poisson_logpmf | 1,000 | 0.04139 | 0.02145 | 0.009005 | 0.03 | 0.0196 |
| poisson_logpmf | 100,000 | 4.105 | 2.305 | 1.26 | 3 | 1.357 |
| poisson_logpmf | 1,000,000 | 40.75 | 21.96 | 12.93 | 30 | 14.08 |
| mean | 1,000 | — | 0.008543 | 0.001493 | 0.002 | 0.001174 |
| mean | 100,000 | — | 0.8752 | 0.1473 | 0.11 | 0.01065 |
| mean | 1,000,000 | — | 8.281 | 1.502 | 1.1 | 0.09985 |
| variance | 1,000 | — | 0.009907 | 0.002825 | 0.0038 | 0.003253 |
| variance | 100,000 | — | 1.009 | 0.2738 | 0.21 | 0.04644 |
| variance | 1,000,000 | — | 10.22 | 2.741 | 2.1 | 0.4446 |
| beta_quantile | 1,000 | 0.9424 | 0.9199 | 0.9109 | 0.39 | 0.3732 |
| beta_quantile | 100,000 | 91.36 | 89.12 | 87.85 | 39 | 35.37 |
| beta_quantile | 1,000,000 | 916.3 | 891.7 | 880.7 | 396 | 353.3 |
| chisquare_cdf | 1,000 | 0.06663 | 0.03809 | 0.03149 | 0.03 | 0.1274 |
| chisquare_cdf | 100,000 | 7.326 | 4.146 | 3.302 | 3 | 13.69 |
| chisquare_cdf | 1,000,000 | 72.36 | 38.72 | 31.75 | 33 | 118 |
Whole-process peak RSS across these workers reached 268.1 MiB; per-worker memory, load/startup times and every timing sample are retained in the raw report.
Timing boundaries and interpretation
Each worker loads/parses inputs once, performs one untimed warm-up, and measures five repetitions. Distribution calls allocate and populate the complete output vector. The PHP scalar loop constructs a distribution and crosses the PHP/native boundary for each value; batch calls construct it once and stream values natively. The internal Rust loop uses the same validation and an allocated result vector. Descriptive mean/variance has one PHP native call; there is no invented scalar-loop equivalent for those operations.
JSON I/O, startup and output summaries are outside timed calls. PHP input/output conversion and native validation are included. R/SciPy start with native numeric vectors. Runtime reclamation/garbage collection of prior results contributes to the observed API cost. Peak RSS is the whole worker process, including imports, JSON parsing, inputs, outputs and summaries, not isolated kernel memory. Process elapsed time and input loading time are recorded separately.
R’s timer has millisecond resolution. Small distribution cases batch 100 calls; mean/variance batch enough calls to process at least ten million input values per measurement, with a minimum of ten calls. Per-call times divide by the batch count. All raw timings must be positive. Thread environment variables are fixed to one for OpenBLAS, OpenMP, MKL, Accelerate and BLIS; they do not make different numerical implementations identical. PHP scalar/batch worker order alternates by case. Machine load, timer precision and GC make these measurements evidence for these workloads rather than universal speed claims.
Seven-model preservation
The preservation report compares the current extension with a rebuilt pre-v0.2 baseline at 6342562, using the same PHP 8.5 headers/compiler. Both receive identical complete designs with intercept insertion disabled where supported. It exercises all seven existing fitters. Reference tolerance is 1e-7 + 1e-6*abs(reference) for the compared inference/scalar outputs, group effects and ten sampled observation outputs. Fits materialize their complete public results before summarization. This is separate from fitting all independent R fixtures in the release gate.
Recorded: 2026-10-12T15:18:25.373311+00:00; baseline revision 6342562. All seven model comparisons passed.
| Fitter | Rows | Largest compared absolute difference | Current/baseline median time |
|---|---|---|---|
| ols | 100,000 | 0 | 1.024× |
| logistic | 100,000 | 0 | 1.024× |
| probit | 100,000 | 0 | 0.974× |
| poisson | 100,000 | 0 | 0.970× |
| lmm | 100,000 | 0 | 0.967× |
| lmm_fit | 1,000 | 0 | 1.006× |
| logistic_mixed | 100,000 | 0 | 0.996× |
This check does not establish bitwise agreement for every observation output or a performance bound on every future workload. Retained regression paths are also covered by the existing Rust/PHP reference and intercept/offset suites.
Reproduction
python3 -m venv .context/v02/python
.context/v02/python/bin/pip install -r tests/benchmarks/distribution-requirements.txt
# R with jsonlite is a development-only reference dependency.
cargo build -p phit --release --locked
cargo build -p stats-core --release --locked --example distribution_bench
PHP=/path/to/matching/php .context/v02/python/bin/python tests/benchmarks/run-distributions.py
python3 scripts/distribution-docs.pyThe runner accepts --sizes, --operations, --repeats, --output and --resume. A partial report is marked complete: false; resume requires matching binary hashes, seed, repetitions, thread settings and input hashes. It keeps completed verification/timings and recomputes any changed input. Use a new output file for a different build. Fixed seed 20261012 and saved NumPy/Python versions specify input generation; SHA-256 records the exact JSON values actually used.
Rebuild the preservation baseline in an isolated source/target directory, using the same PHP/Clang environment, then run:
mkdir -p .context/v02/baseline-src
git archive 6342562 | tar -x -C .context/v02/baseline-src
CARGO_TARGET_DIR="$PWD/.context/v02/baseline-target" cargo build \
--manifest-path .context/v02/baseline-src/Cargo.toml -p phit --release --locked
# macOS; use libphit.so on Linux.
PHP=/path/to/matching/php .context/v02/python/bin/python tests/benchmarks/run-v01.py \
--baseline .context/v02/baseline-target/release/libphit.dylib \
--baseline-revision 6342562 --output docs/benchmarks/v02-preservation-results.json
python3 scripts/distribution-docs.pyThe worker uses reflected function parameters, so the older module name and API remain loadable without importing current stubs into the baseline runtime. No benchmark changes a user’s PHP configuration or needs network/model services.
From phpstats/phit@76b22f0.