https://github.com/opensvm/bmssp-benchmark-game
bounded multi-source shortest paths benches in Rust, C, C++, Nim, Crystal, Kotlin (JAR), Elixir (.exs), Erlang (.erl).
https://github.com/opensvm/bmssp-benchmark-game
benchmark benchmark-game bmssp bounded-multi-source-shortest-paths c cpp crystal elixir erlang graph-algorithms graphs graphs-theory kotlin nim rust
Last synced: 7 months ago
JSON representation
bounded multi-source shortest paths benches in Rust, C, C++, Nim, Crystal, Kotlin (JAR), Elixir (.exs), Erlang (.erl).
- Host: GitHub
- URL: https://github.com/opensvm/bmssp-benchmark-game
- Owner: openSVM
- License: other
- Created: 2025-08-13T05:29:31.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-08-14T02:56:54.000Z (7 months ago)
- Last Synced: 2025-08-14T04:33:03.852Z (7 months ago)
- Topics: benchmark, benchmark-game, bmssp, bounded-multi-source-shortest-paths, c, cpp, crystal, elixir, erlang, graph-algorithms, graphs, graphs-theory, kotlin, nim, rust
- Language: Python
- Homepage:
- Size: 4.86 MB
- Stars: 1
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE-APACHE
Awesome Lists containing this project
README
# bmssp benchmark game
Implementation of the algorithm in multiple languages to compare performance in a standard GitHub Actions environment.
Languages currently wired in this repo: Rust, C, C++, Nim, Crystal, Kotlin (JAR), Elixir (.exs), Erlang (.erl).
## Benchmark snapshot
| impl | lang | graph | n | m | k | B | seed | threads | time_ns | popped | edges_scanned | heap_pushes | B_prime | mem_bytes |
|---------------|--------|-------|------|------|---|----|------|---------|---------|--------|---------------|-------------|---------|-----------|
| rust-bmssp | Rust | grid | 2500 | 9800 | 4 | 50 | 1 | 1 | 741251 | 868 | 3423 | 1047 | 50 | 241824 |
| c-bmssp | C | grid | 2500 | 9800 | 4 | 50 | 1 | 1 | 99065 | 1289 | 5119 | 1565 | 50 | 176800 |
| cpp-bmssp | C++ | grid | 2500 | 9800 | 4 | 50 | 1 | 1 | 117480 | 1064 | 4224 | 1261 | 50 | 176800 |
| kotlin-bmssp | Kotlin | grid | 2500 | 9800 | 4 | 50 | 1 | 1 | 5308820 | 1102 | 4386 | 1309 | 50 | 196800 |
| elixir-bmssp | Elixir | grid | 2500 | 9800 | 4 | 50 | 1 | 1 | 5410039 | 870 | 3447 | 1047 | 50 | 196800 |
| erlang-bmssp | Erlang | grid | 2500 | 9800 | 4 | 50 | 1 | 1 | 1155739 | 691 | 2701 | 818 | 50 | 196800 |
Rust implementation of **bounded multi-source shortest paths** (multi-source Dijkstra cut off at `B`).
## Run
```bash
cargo test
cargo bench -p bmssp
python3 bench/runner.py --release --out results
# fast iteration
python3 bench/runner.py --quick --out results-dev --timeout-seconds 20 --jobs 2
# 1000x larger graphs (heavy):
python3 bench/runner.py --params bench/params_1000x.yaml --release --jobs 4 --timeout-seconds 600 --out results
```
### One-time setup scripts
- Linux/macOS:
- Run `scripts/install_deps.sh --yes` to auto-install Rust, Python deps, build tools, Crystal/shards, Nim.
- Use `--check-only` to only report missing items.
- Windows:
- Open PowerShell (as Administrator recommended) and run `scripts/Install-Dependencies.ps1 -Yes`.
- Add `-CheckOnly` to only report.
## Complexity
Let `U = { v | d(v) < B }` and `E(U)` be edges scanned from `U`.
- Time: `O((|E(U)| + |U|) log |U|)` with binary heap; worst-case `O((m+n) log n)`.
- Space: `Θ(n + m)` graph + `Θ(n)` distances/flags + heap bounded by frontier size.
Use `Graph::memory_estimate_bytes()` to get a byte estimate at runtime.
Here’s the no-BS, self-contained write-up you asked for. It includes the theory, proofs at the right granularity, complexity, comparisons against the usual suspects, and **charts** (model-based, not empirical—use them to reason about trends, not absolutes).
---
# Bounded Multi-Source Shortest Paths (BMSSP)
## What problem it solves
Given a directed graph $G=(V,E)$ with non-negative weights $w:E\to \mathbb{R}_{\ge 0}$, a **set of sources** $S\subseteq V$ with initial offsets $d_0(s)$ (usually 0), and a **distance bound** $B$, compute:
* For every vertex $v$ with true shortest-path distance $d(v) < B$, the exact distance $d(v)$.
* The **explored set** $U=\{v\in V\mid d(v) Multi-source is trivial: seed multiple entries. All standard Dijkstra proofs still apply.
---
## Correctness (sketch you can actually use)
**Invariant** (Dijkstra): When a node $u$ is popped, $\text{dist}[u]=d(u)$.
Proof carries over because (1) weights $\ge 0$, (2) heap order guarantees we never pop a label larger than any alternate path to that node, (3) early exit only **reduces** the set of popped nodes; it never allows popping out of order.
**Boundary lemma.** Let $U$ be nodes popped before exit. Then
$B' = \min\{\hat d(x)\mid x\in V\setminus U\}$ — the smallest tentative distance still in the heap (or discovered and $\ge B$).
So $B' \ge B$ and is the next tight phase boundary if you continue search.
---
## Complexity
Let $U=\{v\mid d(v) Want empirical plots? Run the provided Rust bench across varying $B$, $k=|S|$, $n,m$, and (optionally) a bucketed queue variant. I can script `cargo bench` + CSV + gnuplot for you.
### Crystal implementation
Crystal port lives in `impls/crystal` and follows the same CLI and JSON contract.
Build locally (if Crystal is installed):
```bash
cd impls/crystal
shards build --release
./bin/bmssp_cr --graph grid --rows 20 --cols 20 --k 8 --B 50 --seed 1 --trials 2 --json
```
The bench runner will auto-detect Crystal (`crystal` + `shards` in PATH) and include its results.
---
## Implementation notes that actually matter
* Keep `dist` as `u64`. Use `saturating_add` to avoid overflow during relaxations.
* Don’t attempt “decrease-key”; just push duplicates and skip stale pops. It’s faster with Rust’s `BinaryHeap`.
* Track `B'` while relaxing edges that would cross the bound—this is your next-phase threshold.
* Multi-source is just seeding many `(node, offset)` entries. If you repeat with larger $B$, reuse `dist` as a warm-start and continue; it preserves optimality because labels are monotone.
* Memory: adjacency dominates. The rest is a few `Vec`s of size $n$ and the heap/frontier.
---
## Complexity re-stated (so you don’t scroll back)
* **Time:** $\boxed{ \mathcal{O}\big((|E(U)|+|U|)\log |U|\big) }$
Worst case: $\mathcal{O}((m+n)\log n)$.
* **Space:** $\boxed{ \Theta(n+m) }$ total; working state $\Theta(n)$ + heap $O(|U|)$.
If you want byte-accurate numbers, the Rust crate includes a `memory_estimate_bytes()` helper; for real allocations, measure with `heaptrack`/`massif` or Linux cgroups.
---
## When to prefer other queues
* **Small integer weights** (e.g., 0–255): Dial/radix will beat binary heaps. BMSSP still applies—just stop at $B$—but use buckets.
* **Parallel hardware**: Δ-stepping buckets **plus** bound $B$ works well; you get level-synchronous waves clipped at $B$.
* **Heuristic-rich domains**: A\* with an admissible heuristic can explore **far less than $U$** for the same $B$.
---
## How to produce *real* comparison charts (bench recipe)
* Use the Rust crate I gave you (`cargo bench -p bmssp`) and vary:
* $n,m$ (grid, ER random, power-law)
* number of sources $k$
* bound $B$
* Implement a second queue:
* Dial (if your weights are bounded ints)
* or a bucketed Δ-stepping variant (single-thread first)
* Capture: push counts, relax counts, popped vertices, wall-clock (release), and peak RSS.
* Plot **time vs |U|**, **time vs B**, **pushes per edge**; the trends will match the charts above, with real constants.
---
## TL;DR
* If $B$ is small relative to typical distances, **BMSSP saves you real work**, exactly as the $f\log(fn)$ model predicts.
* If weights are tiny integers, **use buckets** (Dial) and still bound by $B$.
* Otherwise, bounded Dijkstra with a binary heap is the **clean default**—simple, exact, and fast enough.
If you want this rewritten around the **recursive/pivoted BMSSP** from your screenshot (the one with `FindPivots`, batching, etc.), send the full section of the paper and I’ll extend the Rust crate to that variant and give you *measured* charts next.