Investigation: why is CaDiCaL 2.2.1/3.0.0 so much slower than 1.9.5?
Investigation: why is CaDiCaL 2.2.1/3.0.0 so much slower than 1.9.5?
This page is a worked example of using --cadical-version and
--cadical-option (see {doc}cli-reference) to diagnose and fix a real
performance regression, run against a formula with 1572 variables, 169124
clauses, and 288 projected variables (rel_cp.cnf, not included in this
repository).
Initial benchmark: 5-minute soft timeout per version
./just_count --cadical-version 1.9.5 --timeout 300 rel_cp.cnf
./just_count --cadical-version 2.2.1 --timeout 300 rel_cp.cnf
./just_count --cadical-version 3.0.0 --timeout 300 rel_cp.cnf
| CaDiCaL | Result | Time | Models/sec |
|---|---|---|---|
| 1.9.5 | Completed exhaustively: s mc 348535 |
24.4s | ~14,300/s |
| 2.2.1 | Timed out, partial count 145293 | 303.5s | ~479/s |
| 3.0.0 | Timed out, partial count 145293 | 303.8s | ~478/s |
1.9.5 finished the entire count in 24 seconds. 2.2.1 and 3.0.0 both ran out of a 5-minute budget at roughly 1/30th the throughput — and notably found the exact same partial count (145293) at nearly the same wall-clock time, which is a strong hint that whatever changed, changed between 1.9.5 and 2.2.1, and 3.0.0 didn’t change it further.
Bisecting with a short timeout
A 5-minute-per-run feedback loop is too slow for trial and error, so the rest of the investigation used a 2-second soft timeout — short enough for fast iteration, long enough to give a stable throughput signal (dozens to tens of thousands of models, depending on configuration).
./just_count --cadical-version 1.9.5 --timeout 2 -p 0 -q rel_cp.cnf
# c timeout after 2s, models found: 34034 (~17,000/s)
./just_count --cadical-version 2.2.1 --timeout 2 -p 0 -q rel_cp.cnf
# c timeout after 2.00184s, models found: 209 (~104/s)
First guess: newer preprocessing techniques — wrong
CaDiCaL 2.2.1 added several substantial preprocessing/inprocessing
techniques not present in 1.9.5, all enabled by default: sweep (SAT
sweeping), congruence (congruence closure / gate extraction),
backbone (backbone literal computation), plus a condition option
family. Any of these running on every incremental solve() call would
plausibly explain a large per-call slowdown, so the natural first
hypothesis was “one of these is expensive and shouldn’t run every model.”
Testing each individually via --cadical-option:
for opt in sweep=0 congruence=0 backbone=0 condition=0 elim=0 vivify=0 \
probe=0 subsume=0 transred=0 walk=0; do
./just_count --cadical-version 2.2.1 --cadical-option "$opt" \
--timeout 2 -p 0 -q rel_cp.cnf
done
Every single one landed at 200–210 models — indistinguishable from the baseline. Disabling all of them at once didn’t help either (205 models). So none of the “obviously expensive-sounding” new preprocessing techniques were the cause. A longer run (10s, with progress every 500 models) with everything disabled showed a flat ~100–110 models/sec throughout, not a one-time startup cost that would amortize away — this was a genuine per-call regression, not a one-off.
Second guess: ilb — correct
just_count’s workload is unusual for a general-purpose SAT benchmark:
it calls solve() repeatedly, adding exactly one new (blocking)
clause between calls, and expects each subsequent call to be cheap
because almost nothing changed since the last one. That’s precisely the
scenario CaDiCaL’s Incremental Lazy Backtracking (ilb) option
exists for — it avoids backtracking all the way to decision level 0 (and
re-deriving most of the search) on every incremental solve() call.
Comparing the option’s definition between versions
(src/options.hpp):
# CaDiCaL 1.9.5 — boolean, defaults ON
OPTION( ilb, 1, 0, 1, ..., "ILB (incremental lazy backtrack)")
# CaDiCaL 2.2.1 — tri-state, defaults OFF
OPTION( ilb, 0, 0, 2, ..., "ILB (incremental lazy backtrack)
(0: no, 1: assumptions only, 2: everything)")
ilb silently changed from on by default to off by default, and
gained a middle “assumptions only” state that doesn’t help just_count
at all — it doesn’t use assumption literals, only clause addition. The
option just_count needs is ilb=2 (“everything”):
./just_count --cadical-version 2.2.1 --cadical-option ilb=1 \
--timeout 2 -p 0 -q rel_cp.cnf
# 219 models — no better; ilb=1 only reuses the trail for assumptions
./just_count --cadical-version 2.2.1 --cadical-option ilb=2 \
--timeout 2 -p 0 -q rel_cp.cnf
# 13167 models (~6,000/s — about 58x faster than the default)
Confirmed on 3.0.0 too:
./just_count --cadical-version 3.0.0 --cadical-option ilb=2 \
--timeout 2 -p 0 -q rel_cp.cnf
# 13167 models — same fix, same effect
Stacking ilb=2 with the earlier (individually useless) options
The preprocessing techniques that didn’t help on their own turned out
to still be worth disabling once ilb=2 is set — with fast incremental
re-solves doing most of the work, their overhead becomes visible again:
./just_count --cadical-version 2.2.1 --cadical-option ilb=2 \
--cadical-option sweep=0 --cadical-option congruence=0 \
--cadical-option backbone=0 --cadical-option condition=0 \
--cadical-option elim=0 --cadical-option vivify=0 \
--cadical-option probe=0 --cadical-option subsume=0 \
--cadical-option transred=0 --cadical-option walk=0 \
--timeout 2 -p 0 -q rel_cp.cnf
# 49807 models (~24,900/s)
Final validation: full run to completion
| Configuration | Time to s mc 348535 |
|---|---|
| 1.9.5, defaults | ~27–28s |
2.2.1, ilb=2 + the ten options above disabled |
~14s |
Both report the identical exact count (348535), confirming the tuning changes performance, not correctness — and the tuned 2.2.1 run is about 2x faster than 1.9.5’s untouched defaults on this formula.
What actually shipped
The full tuning — ilb=2 plus all ten inprocessing techniques disabled
(sweep, congruence, backbone, condition, elim, vivify,
probe, subsume, transred, walk) — is unconditionally applied in
jc_cadical_new() for every bundled version (see {doc}architecture).
This was initially left partial (ilb=2 only, on the theory that
silently changing what CaDiCaL preprocesses by default was too broad a
change), but re-measuring with only ilb=2 shipped still showed 1.9.5
about 4x ahead of 2.2.1/3.0.0 (148585 vs 35751 models in 10s) — the
newer versions’ extra preprocessing, left at its own defaults, was still
costing more than it recovered. Applying the full profile everywhere
closes that gap and then some:
| CaDiCaL (full tuning baked in) | Models found in 10s | Full run to s mc 348535 |
|---|---|---|
| 1.9.5 | 225915 | ~16.0s |
| 2.2.1 | 245161 | ~14.1s |
| 3.0.0 | 251870 | ~14.1s |
1.9.5 improved too (148585 → 225915 in 10s) since it already has
elim/vivify/probe/subsume/transred/walk even though it
predates sweep/congruence/backbone/condition — but 2.2.1 and
3.0.0 now tie for fastest, both modestly ahead of 1.9.5. All three report
the identical exact count, confirming this doesn’t change correctness.
CADICAL_DEFAULT_VERSION is 3.0.0 — it ties 2.2.1 for fastest and
is the actively maintained release, breaking the tie. --cadical-option
still overrides any of the baked-in options per run (e.g.
--cadical-option sweep=1 to turn SAT sweeping back on).
Takeaways
- When comparing solver versions for an incremental workload
(repeated
solve()calls with small deltas), check options that specifically affect incremental re-solving before suspecting general-purpose preprocessing —ilbhad far more impact here than any of the newer inprocessing techniques, individually or combined. - A short
--timeout(seconds, not minutes) is enough to get a stable throughput signal for this kind of bisection, and made the whole investigation above take a couple of minutes of wall-clock time rather than tens of minutes. --cadical-optiongeneralizes beyond this one finding — it’s a direct pass-through toCaDiCaL::Solver::set(), so any option documented in a given version’ssrc/options.hppcan be tuned from the command line without a rebuild.