Benchmarks

This section presents computational experiments measuring model creation time — the time from an empty model to a fully built, solver-ready instance — across different modelling interfaces, solver backends, and Python interpreters.

Python-MIP communicates every problem modification directly to the solver engine rather than staging a separate intermediate model, buffering column and row additions internally (CFFI C-array caches for CBC and HiGHS, Gurobi’s own update mode) to avoid per-call overhead.

CBC, HiGHS and Gurobi are all accessed through CFFI bindings to the solvers’ native C APIs, and are therefore fully PyPy-compatible. gurobipy itself does not ship a PyPy wheel — python-mip’s CFFI-based Gurobi backend is the only way to drive Gurobi from PyPy, yielding 2.5–5× faster model creation times there depending on model structure (see below).

We compare against raw gurobipy and highspy (HiGHS’s own Python package), including highspy’s recommended vectorized numpy-array API and its lowest-level batch API (a pre-built CSR matrix, the fastest option in every benchmark but with no incremental modelling interface). See Appendix: code clarity, incremental vs. vectorized modelling for a discussion of these trade-offs with side-by-side code.

Experiments were run on a Linux workstation. Reproducible benchmark scripts are in the benchmarks/ directory.

n-Queens

See n-Queens for the problem formulation and example code. The \(n=1200\) instance has 1,440,000 binary variables, 2,400 equality constraints, and up to 4,794 at-most-one diagonal constraints.

The highspy “high-level API” column uses HiGHS’s own recommended vectorized numpy-array style (addBinaries/addConstrs with .sum(axis=...)/.diagonal()), matching the pattern used in HiGHS’s own examples/nqueens.py — not a naive per-variable/per-constraint loop.

Model creation times in seconds — CPython 3.14.4:

\(n\)

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

gurobipy

highspy (hl API)

highspy (batch numpy)

200

0.305

0.252

0.305

0.380

0.268

0.040

400

1.348

1.100

1.272

1.623

1.073

0.164

600

3.162

2.288

3.139

3.725

2.671

0.375

800

5.417

4.430

4.953

7.001

5.046

0.702

1000

>8s

7.087

7.752

>8s

>8s

1.082

1200

>8s

>8s

>8s

>8s

>8s

1.559

Model creation times in seconds — PyPy 3.11 (7.3.20):

gurobipy has no PyPy wheel and is omitted. highspy (both hl and batch APIs) relies heavily on numpy, which PyPy does not JIT-compile — both are included here for completeness but are not representative of PyPy’s strengths.

\(n\)

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

highspy (hl API)

highspy (batch numpy)

200

0.163

0.193

0.139

0.820

0.230

400

0.592

0.467

0.521

2.257

0.553

600

1.357

0.872

0.791

5.315

1.107

800

2.233

1.574

1.529

>8s

2.183

1000

3.651

2.261

2.465

>8s

3.355

1200

5.077

3.619

3.486

>8s

4.447

PyPy delivers a 2–4× speedup over CPython for python-mip model building (smaller than earlier measurements taken on a less loaded machine, but the same direction and order of magnitude).

Once highspy’s vectorized API is used fairly, the picture for CPython is much closer than a naive-API comparison would suggest: python-mip/HiGHS is still the fastest single-variable-at-a-time backend at most sizes, roughly on par with (or a little ahead of) both highspy’s vectorized high-level API and raw gurobipy. The highspy batch numpy API remains far ahead of everything else, as expected for a hand-built CSR bulk call. Under PyPy, highspy’s reliance on numpy works against it — both its APIs degrade sharply, while CBC/HiGHS/Gurobi through python-mip keep scaling smoothly.

Run: python benchmarks/queens_bench.py --build-only

TSP single-commodity flow

Mixed-integer programs using the compact [GaGr78] single-commodity flow formulation for the Travelling Salesman Problem (see The Traveling Salesman Problem for the formulation and example code) on random Euclidean instances (fixed seed for reproducibility).

For \(n\) cities the model has \(2n(n-1)\) variables and \(2n + n(n-1) + (n-1)\) constraints.

The highspy “high-level API” column here uses HiGHS’s vectorized numpy-array API too, but this problem only has arcs \(i \neq j\) — a genuinely sparse index set. To use the dense array API at all, the diagonal entries have to be allocated as real (unused) variables and then fixed to 0 via changeColBounds after the fact. That workaround, and the resulting \(O(n^2)\) dense elementwise arithmetic instead of the natural \(O(n(n-1))\) sparse arc list, is exactly the caveat we expected: it works, but scales worse than the naive per-arc loop once \(n\) grows, and the code is less direct than either python-mip’s dict-of-arcs style or the naive highspy loop.

Model creation times in seconds — CPython 3.14.4:

\(n\)

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

gurobipy

highspy (hl API)

highspy (batch numpy)

30

0.031

0.026

0.034

0.031

0.031

0.003

50

0.074

0.066

0.077

0.091

0.077

0.007

75

0.169

0.152

0.172

0.205

0.201

0.016

100

0.356

0.273

0.332

0.369

0.331

0.042

150

0.811

0.632

0.698

0.804

0.864

0.074

200

1.329

1.204

1.244

1.455

1.803

0.133

300

3.131

2.632

3.249

3.257

5.813

0.399

400

5.923

4.903

5.636

6.147

>8s

0.661

500

>8s

>8s

>8s

>8s

>8s

1.176

Model creation times in seconds — PyPy 3.11 (7.3.20):

gurobipy has no PyPy wheel and is omitted.

\(n\)

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

highspy (hl API)

highspy (batch numpy)

30

0.098

0.093

0.103

0.231

0.014

50

0.106

0.103

0.125

0.418

0.050

75

0.104

0.090

0.118

0.654

0.106

100

0.115

0.074

0.156

1.197

0.139

150

0.249

0.171

0.273

2.472

0.433

200

0.448

0.591

0.304

4.556

0.816

300

1.180

1.293

1.099

>8s

1.541

400

2.110

1.703

1.703

>8s

2.748

500

3.665

3.523

2.656

>8s

4.315

PyPy delivers a solid speedup over CPython for python-mip’s three backends here too (roughly 1.5–3× depending on size), while the numpy-heavy highspy hl variant is markedly worse under PyPy than on CPython — the same pattern seen in n-Queens, amplified by the wasted dense diagonal here.

The TSP flow model interleaves binary and continuous variables with variable-density rows (degree rows touch \(n-1\) variables; capacity rows touch 2; flow-conservation rows touch \(2(n-1)\)). Python-MIP’s cache handles this automatically — no manual CSR construction, and no dense-array workaround for the sparse arc set, required. python-mip/HiGHS is consistently the fastest incremental-API backend here, ahead of gurobipy and both python-mip/CBC and python-mip/Gurobi at most sizes; highspy’s vectorized hl API is competitive up to n ~ 150 but degrades faster than every other backend beyond that, exactly where its dense-array requirement stops fitting this sparse problem. The batch numpy API remains comfortably the fastest, and the only backend that completes n=500 within the 8 s budget.

To verify correctness and solve a small instance: python benchmarks/tsp_flow_bench.py --verify

Run: python benchmarks/tsp_flow_bench.py --build-only

Resource-Constrained Project Scheduling (RCPSP)

Binary integer programs based on the [PWW69] time-indexed formulation (see Resource Constrained Project Scheduling for the formulation and example code). For \(n\) jobs with processing times in \([1,4]\), the time horizon \(T = \sum_j p_j\) grows linearly with \(n\), so the model has \(O(n^2)\) binary variables overall.

Random instances use processing times drawn from \([1,4]\) and are tested in four configurations combining two values for the number of resources (2 or 4) and two values for the number of extra precedence arcs among real jobs (sparse: \(n\) arcs, dense: \(3n\) arcs).

Model creation times in seconds — CPython 3.14.4 (Res = number of resources, Prec = precedence density):

\(n\)

Res

Prec

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

gurobipy

10

2

sparse

0.084

0.013

0.036

0.013

20

2

sparse

0.046

0.047

0.045

0.042

30

2

sparse

0.108

0.116

0.070

0.063

50

2

sparse

0.207

0.237

0.239

0.295

75

2

sparse

0.667

0.588

0.580

0.536

100

2

sparse

1.222

0.997

1.057

0.974

150

2

sparse

2.329

2.665

2.654

2.180

200

2

sparse

4.945

4.824

4.483

3.694

10

2

dense

0.013

0.011

0.011

0.012

20

2

dense

0.041

0.045

0.046

0.062

30

2

dense

0.098

0.120

0.097

0.125

50

2

dense

0.283

0.278

0.273

0.274

75

2

dense

0.887

0.719

0.917

0.708

100

2

dense

1.922

1.708

1.477

1.843

150

2

dense

3.996

3.687

3.113

3.208

200

2

dense

5.621

5.724

7.342

5.686

10

4

sparse

0.017

0.017

0.018

0.022

20

4

sparse

0.070

0.070

0.055

0.032

30

4

sparse

0.085

0.096

0.162

0.133

50

4

sparse

0.367

0.279

0.440

0.358

75

4

sparse

0.880

1.030

0.817

0.690

100

4

sparse

1.768

1.630

1.636

1.155

150

4

sparse

3.902

4.047

3.491

2.686

200

4

sparse

4.949

4.955

5.428

3.800

10

4

dense

0.011

0.011

0.015

0.022

20

4

dense

0.046

0.043

0.048

0.042

30

4

dense

0.122

0.105

0.114

0.092

50

4

dense

0.308

0.326

0.303

0.264

75

4

dense

0.762

0.888

0.798

0.699

100

4

dense

1.428

1.523

1.543

1.171

150

4

dense

3.228

3.269

3.092

2.729

200

4

dense

5.712

5.640

5.573

4.841

Model creation times in seconds — PyPy 3.11 (7.3.20):

\(n\)

Res

Prec

python-mip / CBC

python-mip / HiGHS

30

2

sparse

0.026

0.032

50

2

sparse

0.053

0.060

75

2

sparse

0.119

0.143

100

2

sparse

0.196

0.241

150

2

sparse

0.509

0.464

200

2

sparse

0.810

0.855

30

2

dense

0.018

0.025

50

2

dense

0.077

0.060

75

2

dense

0.155

0.134

100

2

dense

0.342

0.248

150

2

dense

0.578

0.674

200

2

dense

1.109

1.139

30

4

sparse

0.020

0.017

50

4

sparse

0.050

0.046

75

4

sparse

0.128

0.241

100

4

sparse

0.239

0.269

150

4

sparse

0.728

0.664

200

4

sparse

1.144

1.060

30

4

dense

0.021

0.018

50

4

dense

0.054

0.061

75

4

dense

0.313

0.162

100

4

dense

0.318

0.368

150

4

dense

0.705

0.793

200

4

dense

1.284

1.365

PyPy delivers a 4–5× speedup across all RCPSP configurations. The RCPSP model is structurally the most complex of the three benchmarks: it combines binary variables with dense resource-capacity constraints (each touching \(O(n \cdot \bar{p})\) non-zeros) and quadratic scaling in problem size. Python-MIP’s automatic flush policy correctly handles the interleaved variable and constraint additions without any manual CSR construction. gurobipy is consistently 10–25% faster than python-mip/Gurobi at large sizes in this benchmark, reflecting the overhead of the python-mip CFFI abstraction layer for Gurobi; CBC and HiGHS are competitive with gurobipy in most configurations.

Run: python benchmarks/rcpsp_bench.py --build-only

Capacitated Facility Location

Random Euclidean instances of the classic capacitated facility location problem (CFLP) — an applied supply-chain / warehouse-location problem, not a puzzle. For \(n\) facilities and \(n\) customers the model has \(n^2 + n\) variables (an \(n \times n\) continuous assignment matrix plus \(n\) binary open/close variables) and only \(2n\) constraints (one demand row per customer, one aggregated capacity row per facility) — the same “many variables, few constraints” shape as n-Queens, but with dense rows: each demand row touches \(n\) variables and each capacity row touches \(n+1\) variables, and the objective itself sums \(n^2\) terms.

Model creation times in seconds — CPython 3.14.4:

The highspy “high-level API” column uses the vectorized numpy-array API (addBinaries/addVariables/addConstrs) rather than a naive per-variable loop. CFLP’s dense \(n \times n\) assignment structure turns out to be an excellent fit for this style — the capacity/demand rows are natural .sum(axis=...) reductions over a genuinely dense matrix (no diagonal-masking workaround needed, unlike TSP).

\(n\)

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

gurobipy

highspy (hl API)

highspy (batch numpy)

200

0.639

0.578

0.603

0.416

0.264

0.031

400

2.786

2.451

2.579

1.811

1.021

0.159

600

6.790

6.194

6.612

4.092

2.473

0.389

800

>8s

>8s

>8s

>8s

3.883

0.723

1000

>8s

>8s

>8s

>8s

6.442

1.321

1200

>8s

>8s

>8s

>8s

>8s

1.692

Model creation times in seconds — PyPy 3.11 (7.3.20):

gurobipy has no PyPy wheel, so it is omitted from this table — the python-mip / Gurobi column is the only way to drive Gurobi from PyPy.

\(n\)

python-mip / CBC

python-mip / HiGHS

python-mip / Gurobi

highspy (hl API)

highspy (batch numpy)

200

0.310

0.248

0.238

0.562

0.203

400

1.002

0.950

0.970

1.524

0.876

600

2.696

2.279

1.980

3.589

2.014

800

5.017

4.337

3.784

6.560

3.785

1000

>8s

6.672

5.967

>8s

5.400

1200

>8s

>8s

>8s

>8s

>8s

Using gurobipy’s own docstring-idiomatic loop, it beats python-mip/Gurobi on CPython for this benchmark (10–40% at n=200–600, both time out above n=600) — profiling shows the difference is dominated by building large dense Python-level sums (the objective alone sums \(n^2\) terms), a pattern closer to RCPSP’s dense resource rows than to n-Queens’ sparse diagonal constraints. But the fair highspy comparison changes the overall picture substantially: ``highspy``’s vectorized hl API is now the fastest non-batch backend of all, ahead of gurobipy, python-mip/Gurobi, and every other incremental-API option, at every size — and it’s the only non-batch backend that still finishes n=800–1000 within the 8 s budget on CPython. This is the numpy vectorization paying off precisely where it should: a genuinely dense, rectangular assignment structure with elementwise arithmetic (x * d[None, :]) that maps directly onto numpy’s strengths.

Where python-mip still clearly wins for CFLP is PyPy: python-mip/Gurobi on PyPy is roughly 2–3× faster than on CPython (e.g. 6.6s → 2.0s at n=600), and since neither gurobipy (no PyPy wheel at all) nor highspy’s numpy-heavy APIs fare well on PyPy (the vectorized hl API is slower on PyPy than on CPython at every size, the same pattern seen for n-Queens and TSP), python-mip/CBC, /HiGHS and /Gurobi are the only backends that keep scaling well under PyPy for this problem.

The highspy batch numpy API is still the fastest option in absolute terms at large \(n\) (1.7 s at n=1200 while every incremental-API backend times out) — but building the CFLP CSR matrix by hand (_cflp_csr in benchmarks/cflp_bench.py) is roughly 100 lines of manual row/column bookkeeping, versus the ~15-line natural formulation used for python-mip and gurobipy, and about 10 lines for the vectorized highspy hl API. The raw speed of the batch API comes at a real, measurable ergonomics cost that the other three APIs avoid.

Run: python benchmarks/cflp_bench.py --build-only

Appendix: code clarity, incremental vs. vectorized modelling

The tables above establish the performance picture; this appendix backs up the clarity claim with actual code. The short version: python-mip’s incremental, ``model += …`` style is consistently the most direct way to express these models — it maps straight onto the mathematical formulation regardless of whether the underlying structure is dense or sparse. The vectorized ``highspy`` hl API can match that clarity, and its speed, but only for problems that are naturally a dense rectangular array; forcing a sparse problem into it costs both clarity and performance.

n-Queens (dense \(n \times n\) grid) — both styles read naturally:

# python-mip (n-Queens diagonal constraints)
x = [[m.add_var(var_type=BINARY) for j in range(n)] for i in range(n)]
for k in range(-(n - 1), n):
    cells = [x[i][i + k] for i in range(n) if 0 <= i + k < n]
    if len(cells) > 1:
        m += xsum(cells) <= 1

# highspy vectorized hl API — same constraint
x = h.addBinaries(n, n)
for k in range(-(n - 1), n):
    diag = x.diagonal(k)
    if len(diag) > 1:
        h.addConstr(diag.sum() <= 1)

TSP single-commodity flow (sparse index set, arcs \(i \neq j\) only) — python-mip models only the arcs that exist; highspy’s array API must allocate the full dense matrix, including unused diagonal entries, and fix them afterwards:

# python-mip — only real arcs (i != j) exist at all
x = {(i, j): m.add_var(var_type=BINARY)
     for i in range(n) for j in range(n) if i != j}
for i in range(n):
    m += xsum(x[i, j] for j in range(n) if j != i) == 1

# highspy vectorized hl API — must allocate the full dense n x n
# array (including the unused diagonal) to use axis-sum constraints,
# then explicitly disable the diagonal entries afterwards
x = h.addBinaries(n, n)
for i in range(n):
    h.changeColBounds(x[i, i].index, 0, 0)   # arcs i == j don't exist
h.addConstrs(x.sum(axis=1) == 1)

This is the same pattern reflected in the timing tables above: on dense, rectangular problems (n-Queens, CFLP) the vectorized highspy API is about as readable as python-mip and competitive on speed, occasionally even the fastest incremental-style backend. On sparse/irregular problems (TSP) it needs the dummy-diagonal workaround shown above, which is both less direct to write and, once \(n\) grows, slower than python-mip’s direct sparse formulation — because it pays \(O(n^2)\) dense-array cost for a problem that is genuinely \(O(n^2 - n)\) in useful content only.

The highspy batch numpy API (hand-built CSR arrays) is a different category entirely: it is the fastest option in every single benchmark in this document, but it is not “incremental” or “vectorized-but-natural” — it requires writing out row starts, column indices, coefficients and bounds by hand as raw numpy arrays, with no per-variable/per-constraint objects returned, no model += style API, and no ability to add, inspect or modify parts of the model incrementally afterwards. The ~100-line manual CSR construction for CFLP’s _cflp_csr function (versus ~15 lines for the natural python-mip/gurobipy formulation, and ~10 for the vectorized highspy hl API) is a fair representation of that gap in practice.