Serial solver reference profile — 17 August 2026
This report profiles the existing serial Wang solver after making the dense
UNSAT best-failed-leaf snapshot explicitly opt-in. It does not profile an
optimized solver, TaskPlan, or OpenMP implementation; none exists yet.
The results answer which mechanisms dominate the current reference path. They do not by themselves freeze the optimized representation.
Reproduction identity
Baseline parent Git commit:
a00ec08a8fd2ddcd69c542af6787fb3e88049aaa
The measured source did not correspond to a commit. The relevant file identities are therefore included explicitly:
1d171d2e54bd5a742e183def7da22bce4c5bb1ca50ee8d02f6c2f59400ea71bc include/wang/solver.h
23682d8ef5aa305394e9b3dd0997f13e3a49e2854c013334972b048485505173 src/solver/solver_serial.c
5d12e9260cb3028c2507bcd950256b658942393a4babde03acbca13aba32f917 benchmarks/c/bench_solver.c
490ff46c867c066e4d91a97306009aaedb30361932feb9132ebb51c0d7d6b140 benchmarks/run_reference_profile.sh
8fbd23195b4baf6b8f4877e8faedbdc5b16f6460e254e8237b98f3a99bfa6395 Makefile
Environment:
Debian GNU/Linux 13
Linux 6.12.101+deb13-amd64 x86_64
AMD Ryzen 5 3600, 6 cores / 12 threads, boost enabled
CPU governor: schedutil
GCC 14.2.0
C17, -O2, portable build; no -march=native or LTO
Valgrind/Callgrind/Cachegrind 3.24.0
The five timing passes ran with a light but non-isolated host load. CPU frequency was not pinned. Medians are authoritative for this baseline; min/max show the observed noise and are not confidence intervals.
Harness and measurement method
Build and run the complete reference profile with:
make benchmark
The harness prints one key-value record per execution. It separates:
- repeated solves without metrics, used for elapsed time;
- one solve without metrics, used for per-process peak RSS;
- one solve with
WANG_SOLVE_COLLECT_METRICS, used for deterministic work counts rather than timing; - default and
WANG_SOLVE_CAPTURE_UNSAT_SNAPSHOTmodes for the large root conflict.
Each case runs in a distinct process. Solver-only cases construct Region
before starting the timer. End-to-end Yang–Zhang cases include region building,
native solving, the solver’s mandatory witness verification, and destruction.
Peak RSS is getrusage(RUSAGE_SELF).ru_maxrss on a single solve so allocator
retention across repeated solves does not inflate the memory result.
Five complete passes produced identical SAT/UNSAT results and identical metric records in every case.
Corpus v1
| Case | Scope | Expected | Cells | Active | Timing iterations |
|---|---|---|---|---|---|
generic_backtracking_sat |
solver-only | SAT | 16 | 16 | 5,000 |
generic_forced_thin_sat |
solver-only | SAT | 32,768 | 32,768 | 50 |
generic_root_unsat |
solver-only | UNSAT | 2,097,152 | 1 | 5 |
generic_unconstrained_sat |
solver-only | SAT | 9,216 | 9,216 | 5 |
yang_zhang_sat_solver |
solver-only | SAT | 9,361 | 9,345 | 20 |
yang_zhang_unsat_solver |
solver-only | UNSAT | 2,576 | 2,560 | 50 |
yang_zhang_sat_large_solver |
solver-only | SAT | 76,281 | 76,247 | 5 |
yang_zhang_unsat_large_solver |
solver-only | UNSAT | 20,351 | 20,317 | 10 |
matching *_end_to_end cases |
builder + solver | same | same | same | same |
The six-variable satisfiable formula contains (0,1,2) three times and
(3,4,5) three times. The twelve-variable family repeats the same construction
over four disjoint triples. Each variable occurs exactly three times and each
triple admits exactly-one assignments.
The UNSAT family uses disjoint pairs with clauses (a,a,b) and (a,b,b).
Each variable again occurs exactly three times, while the pair requires both
2a+b=1 and a+2b=1, which no Boolean assignment satisfies.
Elapsed-time baseline
Times are milliseconds per iteration from the repeated, metrics-disabled run.
| Case | Mode | Median | Min | Max |
|---|---|---|---|---|
generic_backtracking_sat |
default | 0.032 | 0.032 | 0.033 |
generic_forced_thin_sat |
default | 4.398 | 4.334 | 4.628 |
generic_root_unsat |
default | 19.034 | 18.732 | 19.608 |
generic_root_unsat |
snapshot | 22.109 | 21.298 | 23.021 |
generic_unconstrained_sat |
default | 180.174 | 178.340 | 181.319 |
yang_zhang_sat_solver |
default | 12.191 | 11.911 | 12.559 |
yang_zhang_sat_end_to_end |
default | 12.305 | 12.162 | 12.365 |
yang_zhang_unsat_solver |
default | 3.069 | 3.001 | 3.178 |
yang_zhang_unsat_end_to_end |
default | 3.111 | 3.000 | 3.150 |
yang_zhang_sat_large_solver |
default | 95.255 | 94.239 | 95.898 |
yang_zhang_sat_large_end_to_end |
default | 96.101 | 95.788 | 96.353 |
yang_zhang_unsat_large_solver |
default | 24.211 | 23.588 | 25.496 |
yang_zhang_unsat_large_end_to_end |
default | 25.257 | 24.654 | 25.677 |
For these formula families, builder cost is small relative to solving: the end-to-end median overhead is approximately 0.9 percent for medium SAT, 1.4 percent for medium UNSAT, 0.9 percent for large SAT, and 4.3 percent for large UNSAT. These runs do not support subtracting the two numbers as a precise builder measurement because the difference is small relative to process noise.
Enabling the dense UNSAT snapshot on the 2,097,152-cell root conflict adds about 16.2 percent to the median time. This diagnostic comparison is not an optimization gate for normal solving because the feature is disabled by default.
Single-solve peak RSS
| Case | Mode | Median KiB | Observed range KiB |
|---|---|---|---|
generic_backtracking_sat |
default | 1,588 | 1,468–1,600 |
generic_forced_thin_sat |
default | 3,580 | 3,484–3,632 |
generic_root_unsat |
default | 21,664 | 21,616–21,788 |
generic_root_unsat |
snapshot | 30,024 | 29,788–30,140 |
generic_unconstrained_sat |
default | 3,248 | 3,096–3,352 |
yang_zhang_sat_solver |
default | 2,976 | 2,920–3,152 |
yang_zhang_unsat_solver |
default | 1,956 | 1,932–2,024 |
yang_zhang_sat_large_solver |
default | 14,832 | 14,748–16,668 |
yang_zhang_unsat_large_solver |
default | 4,896 | 4,760–4,944 |
The diagnostic snapshot increases median RSS by 8,360 KiB. The theoretical dense array is 2,097,152 cells times four bytes, or exactly 8,192 KiB; the 168-KiB difference is within process-level measurement noise. This confirms that the new default avoids the intended allocation.
Deterministic solver metrics
End-to-end and solver-only variants have identical solver metrics, so only the solver-only rows are shown.
| Case | Decisions | Backtracks | Reductions | Arcs | MRV scanned | Trail peak | Queue peak | Depth |
|---|---|---|---|---|---|---|---|---|
generic_backtracking_sat |
10 | 2 | 129 | 424 | 83 | 51 | 34 | 8 |
generic_forced_thin_sat |
0 | 0 | 131,071 | 196,599 | 0 | 65,533 | 65,533 | 0 |
generic_root_unsat |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
generic_unconstrained_sat |
9,059 | 0 | 117,098 | 501,298 | 43,897,478 | 80,808 | 27,265 | 9,059 |
yang_zhang_sat_solver |
4 | 0 | 68,548 | 303,875 | 10,708 | 60,290 | 27,791 | 4 |
yang_zhang_unsat_solver |
2 | 2 | 17,564 | 78,048 | 1 | 16,265 | 7,561 | 0 |
yang_zhang_sat_large_solver |
8 | 0 | 572,581 | 2,565,295 | 241,688 | 510,665 | 228,255 | 8 |
yang_zhang_unsat_large_solver |
2 | 2 | 140,502 | 634,603 | 1 | 135,600 | 60,715 | 0 |
The queue and trail peaks are live entry counts, not direct duplicate counts. They justify measuring duplicate queue insertions and repeated trail writes but do not by themselves prove that deduplication is beneficial.
Callgrind and Cachegrind attribution
Callgrind used one metrics-disabled benchmark process per case. Unlike the elapsed solver-only timer, its program total also includes fixture setup and teardown; the percentages below therefore use the stricter whole-process denominator:
generic_unconstrained_sat: 2,271,892,621 instructions; the self cost ofselect_mrv_cell()is 1,905,013,160 instructions, or 83.85 percent;yang_zhang_sat_solver: 116,325,130 instructions; the self cost ofpropagate_queue()is 97,964,477 instructions, or 84.22 percent;yang_zhang_sat_large_solver: 956,473,594 instructions;propagate_queue()remains 84.23 percent at 805,589,942 instructions;generic_forced_thin_sat: 66,080,588 instructions; the serial solver is 85.41 percent inclusive andpropagate_queue()alone is 29.40 percent self.
The phase boundary is also visible on Yang–Zhang SAT. For six variables,
wang_solve_serial() is 97.41 percent inclusive, the builder is 2.43 percent,
and the independent verifier inside the solver is 3.81 percent. At twelve
variables those figures are 97.59, 2.35, and 3.79 percent respectively.
Inclusive percentages overlap because verifier work is part of the solver.
Cachegrind reports:
- unconstrained: 192,940,808 data references, 2.9 percent D1 miss rate and effectively zero aggregate last-level data miss rate;
- six-variable Yang–Zhang SAT: 10,384,328 data references, 0.6 percent D1 and 0.3 percent LLd miss rates.
The measured cases are primarily paying for work volume and instruction count, not a high last-level cache miss rate. Cache simulation is deterministic model evidence, not a substitute for future native hardware-counter measurements.
Conclusions and next gates
The reference baseline exposes two distinct hot regimes:
- Linear MRV dominates a large, weakly constrained search. A derived MRV index or 24 buckets is justified for the performance path, but it will not materially help the profiled Yang–Zhang reductions, which make only 2–8 decisions.
- Propagation dominates both medium and large Yang–Zhang SAT cases at about
84 percent of instructions. This keeps a serial
TaskPlanand later OpenMP propagation in scope, but only after cheaper serial mechanisms and an exact executor-equivalence test. - The reference DFS reserves one
SearchFrameper active cell. On the large Yang–Zhang SAT case this means capacity for 76,247 frames while observed depth is 8. Dynamic performance-path stack capacity is therefore the first low-risk memory candidate. The unconstrained case, whose depth is 9,059 of 9,216 active cells, remains the counterexample that prevents assuming all workloads are shallow. - Queue and trail pressure scale materially: the large Yang–Zhang SAT case
reaches 228,255 pending queue entries and 510,665 live trail entries. Add
direct duplicate/rewrite measurements before implementing
in_queueor trail compaction. - Opt-in UNSAT diagnostics work as intended: default root-UNSAT avoids one complete dense snapshot without changing SAT/UNSAT or scalar failed-leaf metadata.
- The current 95-ms large Yang–Zhang solve is still small enough that OpenMP overhead may dominate. Parallel work requires a predeclared speedup/RSS gate and larger scaling cases; no OpenMP conclusion is made from this baseline.
The evidence supports a separate performance execution path with differential tests and identifies dynamic DFS storage as the lowest-risk first mechanism. MRV indexing and propagation work remain separate measured tracks. No reference-path rewrite is justified by this report.
Verification status
Before recording the profile, the revised solver contract passed:
make check;make strict-check;make sanitizer-checkwith ASan/UBSan/LSan outside the ptrace sandbox;make analyzer-check;make valgrind-check;make cachegrind-check;- ShellCheck for the profiling runner;
- strict compilation and smoke execution of the benchmark harness.
The first sandboxed LSan attempt failed because LeakSanitizer cannot operate under ptrace. The same target completed successfully outside that sandbox; it was an environment limitation, not a sanitizer finding.