Native and Z3 solver comparison benchmark
The comparison runner executes every implemented decision path against a fixed set of versioned Cubic Monotone 1-in-3 SAT files. It records raw samples and summaries as JSON Lines. It is a measurement tool, not a CI performance gate.
Compared engines and scopes
The repository currently has four executable serial paths:
| Engine | wang-solve-verified |
file-to-verified-decision |
|---|---|---|
| native C reference | prepared Region to verified Wang decision |
.cm13 parse, reduction, and verified Wang decision |
| native C optimized | prepared Region to verified Wang decision |
.cm13 parse, reduction, and verified Wang decision |
| Python Boolean Z3 | not applicable | .cm13 parse and copied formula to verified Boolean decision |
| Python Wang Z3 | copied, prepared Region to verified Wang decision |
.cm13 parse, copied formula, reduction, copied region, and verified Wang decision |
wang-solve-verified is the direct comparison on the same reduced Wang
problem. Region construction happens before the timer. The Boolean oracle is
absent because it accepts a Formula, not a Region.
The current public reduction coordinator returns both a Python-owned formula and region after one native parse. The Wang worker uses that public boundary; the formula copy is therefore outside the timer in the prepared-Region scope and inside it in the file scope, even though the Wang oracle itself consumes only the region.
file-to-verified-decision gives all four paths the same input file and checks
the same expected SAT or UNSAT result. It intentionally compares different
pipelines: Boolean Z3 decides the original formula directly, while the other
three paths decide its Yang–Zhang region. Absolute end-to-end measurements are
useful, but a ratio between the Boolean and Wang rows is not a solver speedup
claim.
The OpenMP library is excluded because src/parallel/solver_openmp.c remains a
build scaffold, not an independent solver implementation.
Corpus
The runner exposes three cumulative presets:
| Preset | Cases | Purpose |
|---|---|---|
smoke |
pipeline_sat, pipeline_unsat |
smallest shared SAT and UNSAT files |
standard |
smoke plus yang_zhang_sat_6, yang_zhang_unsat_6 |
extended comparison and timeout evidence |
scaling |
standard plus the 12-variable SAT and UNSAT cases | explicit scaling and timeout evidence |
The four files under benchmarks/instances/ materialize exactly the
deterministic six- and twelve-variable formula families already used by the C
benchmark harness. SAT families repeat each disjoint three-variable clause
three times. UNSAT families use the contradictory pair (x,x,y) and
(x,y,y) for each disjoint variable pair. The runner hashes every input into
each sample record.
These UNSAT inputs are intentionally shallow propagation controls. The smoke
case (x,x,x) is immediately impossible under exact-one position counting;
the larger contradictory pairs are rejected after very shallow Wang search.
They do not stand in for a hard UNSAT family requiring deep backtracking.
Running it
The routine command uses seven fresh samples per engine on the smallest SAT and UNSAT files, with a 30-second timeout per sample:
make benchmark-compare
The standard and scaling presets are intentionally opt-in because Wang Z3 may reach the timeout on the six- and twelve-variable regions:
make build/benchmarks/c/bench_solver shared
uv run --frozen python benchmarks/python/compare_solvers.py \
--preset standard --samples 7 --iterations 1 \
--timeout-seconds 120 \
--c-flags '-std=c17 -Wall -Wextra -Wpedantic -O2' \
> build/benchmarks/solver-comparison.jsonl
Use make benchmark-compare-smoke for a quick correctness exercise. It runs
the smallest UNSAT file once through all applicable engine/scope pairs. CI may
run this smoke target, but it must not impose timing or memory thresholds.
Cases, engines, and scopes can also be selected explicitly with repeatable
--case, --engine, and --scope options. --iterations repeats the measured
operation inside one worker; it defaults to one so expensive and timed-out
cases remain visible rather than being amortized away.
Measurement protocol
Every sample runs in a fresh child process. The controller reverses the full engine/scope order on alternating samples to reduce fixed ordering bias. There is no hidden warm-up pass. Python imports, shared-library loading, and process startup occur before the timer and are therefore excluded from elapsed time. The timeout is an external wall-clock limit around the entire worker, including startup and preparation, so it can always terminate a stuck or oversized run.
Within wang-solve-verified, parsing and region preparation are also outside
the timer. The timed operation is the solver plus validation of a SAT witness;
UNSAT is checked against the expected result. Within
file-to-verified-decision, parsing and every model copy or reduction needed by
that path are inside the timer, followed by solving and result validation.
process_peak_rss_kib is the high-water resident set for the entire fresh
worker, not only the timed interval. Linux workers read VmHWM from
/proc/self/status; the portable fallback is getrusage. Runtime overhead,
imports, and prepared inputs therefore remain part of memory consumption even
when they are excluded from elapsed time. Memory rows across C, CPython, and Z3
describe whole-process cost and should be interpreted accordingly.
A timed-out worker produces a TIMEOUT sample with null timing and memory.
Timeouts are counted but censored from median, minimum, and maximum calculations;
they must be reported alongside completed samples and never converted into an
invented duration or performance ratio.
JSON Lines records
The first record is environment and contains schema/suite versions, commit
and dirty state, host/runtime/compiler identity, recorded C flags, CPU affinity,
selected cases and scopes, sample count, iterations, and timeout. It is followed
by one sample record per execution and one summary record per
case/engine/scope group.
Raw samples are the evidence of record. Summaries contain completed/timeout counts, median/minimum/maximum nanoseconds per iteration, and median process peak RSS. Host load, CPU frequency, affinity, compiler flags, dependency versions, commit, corpus hashes, and timeout policy must remain fixed before two runs are treated as a controlled comparison.
Recorded baseline
smoke baseline records the first seven-sample, CPU-pinned smoke baseline, including complete timing ranges, process RSS, correctness results, the shallow-UNSAT explanation, and the limits on cross-problem interpretation.
The later Wang Z3 edge-table report records the oracle’s constraint-model refactor and its directly comparable single-sample before/after check. It does not rewrite the historical baseline or introduce a timing threshold.