⏱️ Timing Bertini 1 vs Bertini 2¶
Bertini 2 is a from-scratch C++ and Python rewrite of Bertini 1. We started the rewrite in about 2014. A fair question is how its speed compares to Bertini 1.7. This tutorial benchmarks the two on the same systems and ends in a plot — and records what was timed and where, so the numbers can be refreshed as the library improves.
The comparison¶
The fair way to compare two solvers is to hand them the same problem with the same settings. We
build each system once in pybertini and emit it as a Bertini-1 classic input with
System.to_classic_input(), which writes the equations and the tracking knobs (precision
mode, predictor, tolerances, step cadence). Both solvers read that one file, each builds its own
random start system, and solves. We time only the solver subprocess (never parsing/IO) and keep the
fastest of a few repeats, serial (BERTINI_NUM_THREADS=1):
def time_solver(exe, input_text, repeats=3, timeout=600):
"""Fastest serial wall time (seconds) over `repeats` runs of `exe` on `input_text`; (time, count)."""
best, count = float("inf"), None
for _ in range(repeats):
d = tempfile.mkdtemp()
try:
open(os.path.join(d, "input"), "w").write(input_text)
env = dict(os.environ, BERTINI_NUM_THREADS="1")
t0 = time.perf_counter()
r = subprocess.run([os.path.abspath(exe), "input"], cwd=d, env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=timeout)
dt = time.perf_counter() - t0
if r.returncode == 0:
best = min(best, dt)
count = solution_count(d)
finally:
shutil.rmtree(d, ignore_errors=True)
return (best if best < float("inf") else None), count
(The maintained, fuller harness — which also sweeps MPI ranks and appends a committed history.csv
— is benchmark/comparison/run_comparison.py; this tutorial is a small self-contained cousin.)
Results¶
Run it (needs matplotlib; Bertini 1 optional):
python python/docs/source/tutorials/bertini1_vs_bertini2_timing/b1_vs_b2_timing.py \
--bertini2 ./build/core/bertini2 --bertini1 /usr/local/bin/bertini
How to read it (for the run shown — see the caption for date/versions/machine):
Bertini 2 is still slower than Bertini 1 on most of these problems — level on the tiny diagonal system, up to about 4.4× on
diag-6. This is honest: Bertini 1 is hand-tuned C with its own linear algebra; Bertini 2 trades constant-factor speed for a templated, observable design. Silviana knows some low-hanging fruit to be able to go further (eliminating more linear algebra in Eigen, because Eigen causes churn for allocated types likecomplex_mp.)The diagonal family
diag-3/5/6is well-conditioned (every path stays in double precision), so its growing slowdown is pure per-step tracking overhead scaling with problem size — a standing performance lever, independent of the endgame.cyclic-5exercises the endgame. The adaptive-numeric-type endgame (the Cauchy and PowerSeries endgames now compute in hardwarecomplex_dblwhile a path’s conditioning allows, escalating to mpfr only when the tracker’s authority demands it) cut the endgame’s per-operation cost by roughly 10×. Where a path’s endgame stays in double — e.g. the default TotalDegreeBinomial start system — cyclic-5 in adaptive precision is now ~Bertini-1 speed (~0.4 s serial, with 0 of 70 finite paths needing to escalate above double).This benchmark hands both solvers a total-degree classic input instead, and on it Bertini 2 takes about 1.8× Bertini 1’s time on cyclic-5 — well below the diagonal family’s 4.4× at
diag-6.
Both solvers report the same solution counts (shown under the bars), so this is a like-for-like comparison, not a speed/accuracy trade.
Some record keeping¶
Timing numbers are meaningless without context, and they go stale. We capture the date, both solver versions (with the Bertini 2 git commit), and the machine, and stamp them onto the figure:
def provenance(b2_exe, b1_exe):
"""Record WHAT was timed and WHERE, so the numbers can be refreshed and stay interpretable."""
here = os.path.dirname(os.path.abspath(__file__))
try:
commit = subprocess.run(["git", "-C", here, "rev-parse", "--short", "HEAD"],
capture_output=True, text=True).stdout.strip()
except Exception:
commit = ""
cpu = platform.processor() or "unknown"
try:
if platform.system() == "Darwin":
cpu = subprocess.run(["sysctl", "-n", "machdep.cpu.brand_string"],
capture_output=True, text=True).stdout.strip() or cpu
except Exception:
pass
b2 = _first_nonempty_version_line(b2_exe) + (" @ " + commit if commit else "")
return {"date": datetime.date.today().isoformat(),
"b2": b2,
"b1": _first_nonempty_version_line(b1_exe),
"machine": "{} / {} {}".format(cpu, platform.system(), platform.release())}