Measurement Methodology¶
The performance results documented on the architecture page matter less than the method that produced them. An inference engine's worst failure mode under optimization pressure is silent semantic drift โ a candidate set that is almost complete, a cache that is usually fresh. This page records the discipline that kept zelph's optimization work honest: falsifiable predictions, exact counter identities, bit-stable invariants, and independent semantic nets. It is written so that future engine changes can be held to the same standard.
Falsifiable Predictions First¶
Every increment starts with a written bet before the measurement: a predicted timing band, the counters expected to stay bit-identical, and the counters expected to drift โ with the mechanism that explains the drift and its rough magnitude. The numbers then overrule the bet. Missed predictions are the valuable ones: a change that speeds nothing up, or a counter that moves without a mechanism, has just told you your model of the engine is wrong โ which is precisely what the next profile must resolve before more code is written. Expect calibration to take several misses in both directions; keep betting anyway.
Counter Mode and .prof¶
The reasoning profiler (reasoning_profiler.hpp) is enabled by .log -1:
counter-only mode, which accumulates all counters without emitting a
single log line. This matters twice over. First, measurement purity โ log
rendering locks and allocates, and at ten thousand deductions it dominates
what you are trying to measure; never profile with .log 1 or deeper.
Second, production purity โ every counter increment is gated on
logging_active(), so unlogged runs pay nothing. In counter mode the
window accumulates across statements and imports; .prof dumps it on
demand and .prof reset starts a fresh window between phases. Use
.deductions off as well: rendering large derived terms dominates
wall-clock time otherwise (see
Deduction Output Modes).
The Reference Protocol¶
The standing reference workload is the Jacobian pair
(dev_scripts/test-jacobian-var for the differentiation phase,
dev_scripts/test-jacobian-a for the determinant phase), run in a fresh
REPL:
.deductions off
.log -1
.import dev_scripts/test-jacobian-var
.prof reset
.import dev_scripts/test-jacobian-a
.prof
The -- N ms -- timer after each import is the phase timing; the .prof
dumps are the counter protocol. The full test suite (timed as a whole) is
the third standing number. Always compare fresh-REPL runs โ counters and
caches accumulate by design.
Identities and Invariants¶
Two classes of counter relations are checked on every protocol.
Exact identities must hold to the last digit. fs_cache misses equals
genuine hits + genuine walks โ every cache miss is answered by exactly one
of the two layers below it. On store-armed workloads, all three walk
counters (genuine walks, var_closure walk_fallbacks, template_vars
walks) are zero; any nonzero value means the disarm funnel fired
mid-run, and the measurement stops until the trigger is understood.
Hard semantic invariants must be bit-identical across any
semantics-neutral change: facts_created, seminaive_seeds,
extract ok and template_rejects, the entire evaluate, negation,
check_fact and termination_guard blocks, and
seminaive_safety_extra=0. These counters describe what was derived and
which decisions the engine took; an optimization that moves them has
changed semantics, whatever the test suite says.
Drift Families¶
Counters outside the invariant set drift, and every family has a known
mechanism: parallel= and scanned(par) vary with nondeterministic
parallel launches; scanned(seq), snapshot_facts and the unify()
family drift by dozens because the parallel match-queue drain order
perturbs fact-creation order, so snapshots taken mid-run see slightly
different candidate sets; fs_cache hits/misses and stale_erased drift
by a few thousand for the same reason. The rule is not "small drift is
fine" โ it is every drift needs a mechanism. A change may legitimately
move a counter far outside its family when the mechanism predicts it
exactly (hoisting the pattern decomposition out of the Unification
constructor removed one to two cache probes per seed, and fs_cache hits
dropped by the predicted several hundred thousand). Unexplained drift, of
any size, is a finding.
The Semantic Nets¶
Counter protocols catch regressions in the measured workload; three
independent nets catch them everywhere else. The test suite runs
permanently in .semi-naive check mode, so every case is verified by
classic evaluation passes against the seeded fixpoint โ the completeness
net for the entire delta/anchoring machinery, and the reason "accepted
divergence class" is an admissible phrase on the architecture page at all.
.anchors off provides the anchor-free naive reference for suspected
anchoring bugs. And the suite itself multiplies coverage across both
parallelism modes and all three arithmetic substrates, so
representation-agnosticism is continuously enforced rather than assumed.
CPU Profiles¶
Wall-clock attribution comes from perf on a dedicated build tree with
release flags plus -g:
# nushell
".deductions off\n.import dev_scripts/test-jacobian-var\n.quit\n"
| perf record -o perf-diffby.data build-prof/bin/zelph
# bash / zsh
printf '.deductions off\n.import dev_scripts/test-jacobian-var\n.quit\n' \
| perf record -o perf-diffby.data build-prof/bin/zelph
perf report -i perf-diffby.data --no-children --percent-limit 0.5 --stdio | head -n 80
On hybrid CPUs, judge by the cpu_core block: the cpu_atom block is
dominated by pool-worker idle churn (futex and scheduler symbols) and its
kernel lines push the interesting samples down โ hence the generous
head -n 80. Read profiles together with the counters: self-time tells you
where cycles go, counters tell you how often and whether the work was
necessary; conclusions drawn from only one of the two have repeatedly been
wrong.
Known Honesty Gaps¶
The profiler does not count get_fact_structures calls on the parallel
scan path (rare, and visible as a small calls/scanned discrepancy).
Sub-second phase timings carry tens of milliseconds of run-to-run noise โ
compare bands, not single runs. And at the current state, script parsing
(the Janet/PEG layer) is a visible floor of the reference timings: a
measurement that "improves" it without touching the parser is measuring
noise. When the honest expected gain of the next increment drops into that
noise floor, the correct optimization is to stop.