phit

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.py

The 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.py

The 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.