diff --git a/.github/workflows/iai-callgrind.yml b/.github/workflows/iai-callgrind.yml
new file mode 100644
index 00000000..30e2fc88
--- /dev/null
+++ b/.github/workflows/iai-callgrind.yml
@@ -0,0 +1,230 @@
+name: iai-callgrind Benchmarks
+
+on:
+ pull_request:
+ merge_group:
+
+jobs:
+ benchmarks:
+ name: Run iai-callgrind benchmarks
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: actions/checkout@v6
+ name: Checkout PR branch
+ with:
+ fetch-depth: 0
+
+ - name: Install Rust toolchain
+ run: |
+ rustup install --profile minimal stable
+ rustup default stable
+
+ - name: Install valgrind
+ run: sudo apt-get update && sudo apt-get install -y valgrind
+
+ - name: Install iai-callgrind-runner
+ uses: baptiste0928/cargo-install@v3
+ with:
+ crate: iai-callgrind-runner
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ key: iai-callgrind
+
+ - name: Get base branch name
+ id: base_branch
+ run: |
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ echo "name=${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
+ else
+ echo "name=main" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Checkout base branch
+ run: |
+ git fetch origin ${{ steps.base_branch.outputs.name }}
+ git checkout origin/${{ steps.base_branch.outputs.name }}
+
+ - name: Run benchmarks on base branch
+ continue-on-error: true
+ run: |
+ echo "Running benchmarks on base branch: ${{ steps.base_branch.outputs.name }}"
+ cargo bench --features iai --bench iai_algos --bench iai_edmondskarp --bench iai_kuhn_munkres --bench iai_separate_components 2>&1 | tee baseline-output.txt
+
+ - name: Checkout PR branch
+ run: git checkout ${{ github.sha }}
+
+ - name: Clear target directory for PR build
+ run: cargo clean
+
+ - name: Run benchmarks on PR branch
+ run: |
+ echo "Running benchmarks on PR branch"
+ cargo bench --features iai --bench iai_algos --bench iai_edmondskarp --bench iai_kuhn_munkres --bench iai_separate_components 2>&1 | tee pr-output.txt
+
+ - name: Parse and compare results
+ if: github.event_name == 'pull_request'
+ id: parse_results
+ run: |
+ python3 << 'EOF'
+ import re
+ import os
+
+ def parse_benchmark_output(filename):
+ """Parse iai-callgrind output and extract benchmark results."""
+ benchmarks = {}
+ try:
+ with open(filename, 'r') as f:
+ content = f.read()
+
+ # Pattern to match benchmark names and their metrics
+ benchmark_pattern = r'([^\n]+?)::[^\n]+?::([^\n]+?)\n\s+Instructions:\s+(\d+)'
+
+ for match in re.finditer(benchmark_pattern, content):
+ bench_name = f"{match.group(1)}::{match.group(2)}"
+ instructions = int(match.group(3))
+ benchmarks[bench_name] = instructions
+ except FileNotFoundError:
+ pass
+
+ return benchmarks
+
+ baseline = parse_benchmark_output('baseline-output.txt')
+ pr_results = parse_benchmark_output('pr-output.txt')
+
+ # Create markdown comment
+ comment = "## 📊 iai-callgrind Benchmark Results\n\n"
+
+ if not baseline:
+ comment += "⚠️ **No baseline benchmarks found.** This may be the first time these benchmarks are run on the base branch.\n\n"
+ comment += "### PR Branch Results\n\n"
+ comment += "| Benchmark | Instructions |\n"
+ comment += "|-----------|-------------|\n"
+ for name, instr in sorted(pr_results.items()):
+ comment += f"| `{name}` | {instr:,} |\n"
+ else:
+ # Compare results
+ improvements = []
+ regressions = []
+ unchanged = []
+ new_benchmarks = []
+
+ for name, pr_instr in sorted(pr_results.items()):
+ if name in baseline:
+ base_instr = baseline[name]
+ diff = pr_instr - base_instr
+ pct_change = (diff / base_instr) * 100 if base_instr > 0 else 0
+
+ result = {
+ 'name': name,
+ 'base': base_instr,
+ 'pr': pr_instr,
+ 'diff': diff,
+ 'pct': pct_change
+ }
+
+ if abs(pct_change) < 0.1: # Less than 0.1% change
+ unchanged.append(result)
+ elif diff < 0:
+ improvements.append(result)
+ else:
+ regressions.append(result)
+ else:
+ new_benchmarks.append({'name': name, 'pr': pr_instr})
+
+ # Summary
+ if regressions:
+ comment += f"### ⚠️ {len(regressions)} Regression(s) Detected\n\n"
+ comment += "| Benchmark | Base | PR | Change | % |\n"
+ comment += "|-----------|------|----|---------|\n"
+ for r in sorted(regressions, key=lambda x: abs(x['pct']), reverse=True):
+ comment += f"| `{r['name']}` | {r['base']:,} | {r['pr']:,} | +{r['diff']:,} | +{r['pct']:.2f}% |\n"
+ comment += "\n"
+
+ if improvements:
+ comment += f"### ✅ {len(improvements)} Improvement(s)\n\n"
+ comment += "| Benchmark | Base | PR | Change | % |\n"
+ comment += "|-----------|------|----|---------|\n"
+ for r in sorted(improvements, key=lambda x: abs(x['pct']), reverse=True):
+ comment += f"| `{r['name']}` | {r['base']:,} | {r['pr']:,} | {r['diff']:,} | {r['pct']:.2f}% |\n"
+ comment += "\n"
+
+ if unchanged:
+ comment += f"### ➡️ {len(unchanged)} Unchanged (within ±0.1%)\n\n"
+ comment += "Click to expand
\n\n"
+ comment += "| Benchmark | Instructions |\n"
+ comment += "|-----------|-------------|\n"
+ for r in unchanged:
+ comment += f"| `{r['name']}` | {r['pr']:,} |\n"
+ comment += "\n \n\n"
+
+ if new_benchmarks:
+ comment += f"### 🆕 {len(new_benchmarks)} New Benchmark(s)\n\n"
+ comment += "| Benchmark | Instructions |\n"
+ comment += "|-----------|-------------|\n"
+ for nb in new_benchmarks:
+ comment += f"| `{nb['name']}` | {nb['pr']:,} |\n"
+ comment += "\n"
+
+ if not regressions and not improvements and not new_benchmarks:
+ comment += "### ✅ All benchmarks unchanged\n\n"
+
+ comment += "\n---\n"
+ comment += "*iai-callgrind measures instructions executed, which is deterministic and not affected by system load.*\n"
+
+ # Write to file
+ with open('comment.txt', 'w') as f:
+ f.write(comment)
+
+ print("Comment generated successfully")
+ EOF
+
+ - name: Post comment to PR
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const comment = fs.readFileSync('comment.txt', 'utf8');
+
+ // Find existing comment
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const botComment = comments.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes('iai-callgrind Benchmark Results')
+ );
+
+ if (botComment) {
+ // Update existing comment
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: comment
+ });
+ } else {
+ // Create new comment
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: comment
+ });
+ }
+
+ - name: Add summary
+ if: always()
+ run: |
+ if [ -f comment.txt ]; then
+ cat comment.txt >> $GITHUB_STEP_SUMMARY
+ else
+ echo "## Benchmark Results" >> $GITHUB_STEP_SUMMARY
+ echo "Benchmark comparison was not generated." >> $GITHUB_STEP_SUMMARY
+ fi
diff --git a/Cargo.lock b/Cargo.lock
index 6bfe3111..cd893650 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -44,6 +44,15 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "bitflags"
version = "2.10.0"
@@ -257,6 +266,27 @@ dependencies = [
"syn",
]
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn",
+]
+
[[package]]
name = "either"
version = "1.15.0"
@@ -327,6 +357,42 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+[[package]]
+name = "iai-callgrind"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1e4910d3a9137442723dfb772c32dc10674c4181ca078d2fd227cd5dce9db0"
+dependencies = [
+ "bincode",
+ "derive_more",
+ "iai-callgrind-macros",
+ "iai-callgrind-runner",
+]
+
+[[package]]
+name = "iai-callgrind-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d03775318d3f9f01b39ac6612b01464006dc397a654a89dd57df2fd34fb68c3"
+dependencies = [
+ "derive_more",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn",
+]
+
+[[package]]
+name = "iai-callgrind-runner"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b74c9743c00c3bca4aaffc69c87cae56837796cd362438daf354a3f785788c68"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "indexmap"
version = "2.12.1"
@@ -469,6 +535,7 @@ version = "4.14.0"
dependencies = [
"codspeed-criterion-compat",
"deprecate-until",
+ "iai-callgrind",
"indexmap",
"integer-sqrt",
"itertools 0.14.0",
@@ -521,6 +588,28 @@ dependencies = [
"zerocopy",
]
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.101"
@@ -638,6 +727,15 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
diff --git a/Cargo.toml b/Cargo.toml
index 258db6a8..9ca98786 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -33,6 +33,9 @@ pre-release-replacements = [
{file = "CHANGELOG.md", search = "n\\.n\\.n", replace = "{{tag_name}}", exactly = 1}
]
+[features]
+iai = []
+
[dependencies]
num-traits = "0.2.19"
indexmap = "2.11.4"
@@ -43,6 +46,7 @@ deprecate-until = "0.1.1"
[dev-dependencies]
codspeed-criterion-compat = "4.0.0"
+iai-callgrind = "0.16.1"
itertools = "0.14.0"
movingai = "2.1.0"
noisy_float = "0.2.0"
@@ -90,3 +94,23 @@ harness = false
[[bench]]
name = "matrices"
harness = false
+
+[[bench]]
+name = "iai_algos"
+harness = false
+required-features = ["iai"]
+
+[[bench]]
+name = "iai_edmondskarp"
+harness = false
+required-features = ["iai"]
+
+[[bench]]
+name = "iai_kuhn_munkres"
+harness = false
+required-features = ["iai"]
+
+[[bench]]
+name = "iai_separate_components"
+harness = false
+required-features = ["iai"]
diff --git a/README.md b/README.md
index 3d525a05..a0ecd321 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,32 @@ If you want to use this library with traditional graph structures (nodes, edges,
This code is released under a dual Apache 2.0 / MIT free software license.
+## Benchmarking
+
+This repository includes two types of benchmarks:
+
+### Wall-time Benchmarks (Criterion/CodSpeed)
+
+Traditional wall-time benchmarks using Criterion (with CodSpeed compatibility) are located in `benches/` with names like `algos.rs`, `edmondskarp.rs`, etc. These can be run with:
+
+```bash
+cargo bench --bench algos --bench edmondskarp --bench kuhn_munkres --bench separate_components
+```
+
+### Deterministic Benchmarks (iai-callgrind)
+
+For more precise and deterministic performance measurements, we use iai-callgrind which counts CPU instructions, cache hits/misses, and estimated cycles using Valgrind. These benchmarks are prefixed with `iai_` and require the `iai` feature flag:
+
+```bash
+# Install valgrind first (required by iai-callgrind)
+sudo apt-get install valgrind # On Ubuntu/Debian
+
+# Run the benchmarks with the feature flag
+cargo bench --features iai --bench iai_algos --bench iai_edmondskarp --bench iai_kuhn_munkres --bench iai_separate_components
+```
+
+The iai-callgrind benchmarks provide consistent results across runs and are not affected by system load, making them ideal for detecting performance regressions. They run automatically in CI for all pull requests, comparing performance against the base branch.
+
## Contributing
You are welcome to contribute by opening [issues](https://github.com/evenfurther/pathfinding/issues)
diff --git a/benches/iai_algos.rs b/benches/iai_algos.rs
new file mode 100644
index 00000000..49dac531
--- /dev/null
+++ b/benches/iai_algos.rs
@@ -0,0 +1,207 @@
+use iai_callgrind::{library_benchmark, library_benchmark_group, main};
+use pathfinding::prelude::{astar, bfs, bfs_bidirectional, dfs, dijkstra, fringe, idastar, iddfs};
+
+#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+struct Pt {
+ x: u16,
+ y: u16,
+}
+
+impl Pt {
+ const fn new(x: u16, y: u16) -> Self {
+ Self { x, y }
+ }
+
+ #[inline]
+ const fn heuristic(p: &Self) -> usize {
+ (128 - p.x - p.y) as usize
+ }
+}
+
+#[inline]
+fn successors(pt: &Pt) -> Vec {
+ let mut ret = Vec::with_capacity(4);
+ if 0 < pt.x {
+ ret.push(Pt::new(pt.x - 1, pt.y));
+ }
+ if pt.x < 64 {
+ ret.push(Pt::new(pt.x + 1, pt.y));
+ }
+ if 0 < pt.y {
+ ret.push(Pt::new(pt.x, pt.y - 1));
+ }
+ if pt.y < 64 {
+ ret.push(Pt::new(pt.x, pt.y + 1));
+ }
+ ret
+}
+
+#[library_benchmark]
+fn corner_to_corner_astar() {
+ assert_ne!(
+ astar(
+ &Pt::new(0, 0),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ Pt::heuristic,
+ |n| n.x == 64 && n.y == 64,
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_bfs() {
+ assert_ne!(
+ bfs(&Pt::new(0, 0), successors, |n| n.x == 64 && n.y == 64),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_bfs_bidirectional() {
+ assert_ne!(
+ bfs_bidirectional(&Pt::new(0, 0), &Pt::new(64, 64), successors, successors),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_dfs() {
+ assert_ne!(
+ dfs(Pt::new(0, 0), successors, |n| n.x == 64 && n.y == 64),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_dijkstra() {
+ assert_ne!(
+ dijkstra(
+ &Pt::new(0, 0),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ |n| n.x == 64 && n.y == 64,
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_fringe() {
+ assert_ne!(
+ fringe(
+ &Pt::new(0, 0),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ Pt::heuristic,
+ |n| n.x == 64 && n.y == 64,
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_idastar() {
+ assert_ne!(
+ idastar(
+ &Pt::new(0, 0),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ Pt::heuristic,
+ |n| n.x == 64 && n.y == 64,
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn corner_to_corner_iddfs() {
+ assert_ne!(
+ iddfs(Pt::new(0, 0), successors, |n| n.x == 5 && n.y == 5),
+ None
+ );
+}
+
+#[library_benchmark]
+fn no_path_astar() {
+ assert_eq!(
+ astar(
+ &Pt::new(2, 3),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ |_| 1,
+ |_| false,
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn no_path_bfs() {
+ assert_eq!(bfs(&Pt::new(2, 3), successors, |_| false), None);
+}
+
+#[library_benchmark]
+fn no_path_bfs_bidirectional() {
+ assert_eq!(
+ bfs_bidirectional(
+ &Pt::new(2, 3),
+ &Pt::new(u16::MAX, u16::MAX),
+ successors,
+ |_| vec![]
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn no_path_dfs() {
+ assert_eq!(dfs(Pt::new(2, 3), successors, |_| false), None);
+}
+
+#[library_benchmark]
+fn no_path_dijkstra() {
+ assert_eq!(
+ dijkstra(
+ &Pt::new(2, 3),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ |_| false,
+ ),
+ None
+ );
+}
+
+#[library_benchmark]
+fn no_path_fringe() {
+ assert_eq!(
+ fringe(
+ &Pt::new(2, 3),
+ |n| successors(n).into_iter().map(|n| (n, 1)),
+ |_| 1,
+ |_| false,
+ ),
+ None
+ );
+}
+
+library_benchmark_group!(
+ name = corner_to_corner;
+ benchmarks =
+ corner_to_corner_astar,
+ corner_to_corner_bfs,
+ corner_to_corner_bfs_bidirectional,
+ corner_to_corner_dfs,
+ corner_to_corner_dijkstra,
+ corner_to_corner_fringe,
+ corner_to_corner_idastar,
+ corner_to_corner_iddfs
+);
+
+library_benchmark_group!(
+ name = no_path;
+ benchmarks =
+ no_path_astar,
+ no_path_bfs,
+ no_path_bfs_bidirectional,
+ no_path_dfs,
+ no_path_dijkstra,
+ no_path_fringe
+);
+
+main!(library_benchmark_groups = corner_to_corner, no_path);
diff --git a/benches/iai_edmondskarp.rs b/benches/iai_edmondskarp.rs
new file mode 100644
index 00000000..83541ac0
--- /dev/null
+++ b/benches/iai_edmondskarp.rs
@@ -0,0 +1,68 @@
+use iai_callgrind::{library_benchmark, library_benchmark_group, main};
+use pathfinding::directed::edmonds_karp::{DenseCapacity, EKFlows, SparseCapacity, edmonds_karp};
+use std::collections::HashMap;
+
+/// Return a list of edges with their capacities.
+fn successors_wikipedia() -> Vec<((char, char), i32)> {
+ vec![
+ ("AB", 3),
+ ("AD", 3),
+ ("BC", 4),
+ ("CA", 3),
+ ("CD", 1),
+ ("CE", 2),
+ ("DE", 2),
+ ("DF", 6),
+ ("EB", 1),
+ ("EG", 1),
+ ("FG", 9),
+ ]
+ .into_iter()
+ .map(|(s, c)| {
+ let mut name = s.chars();
+ ((name.next().unwrap(), name.next().unwrap()), c)
+ })
+ .collect()
+}
+
+fn check_wikipedia_result(flows: EKFlows) {
+ let (caps, total, _cuts) = flows;
+ assert_eq!(caps.len(), 8);
+ let caps = caps.into_iter().collect::>();
+ assert_eq!(caps[&('A', 'B')], 2);
+ assert_eq!(caps[&('A', 'D')], 3);
+ assert_eq!(caps[&('B', 'C')], 2);
+ assert_eq!(caps[&('C', 'D')], 1);
+ assert_eq!(caps[&('C', 'E')], 1);
+ assert_eq!(caps[&('D', 'F')], 4);
+ assert_eq!(caps[&('E', 'G')], 1);
+ assert_eq!(caps[&('F', 'G')], 4);
+ assert_eq!(total, 5);
+}
+
+#[library_benchmark]
+fn wikipedia_example_dense() {
+ check_wikipedia_result(edmonds_karp::<_, _, _, DenseCapacity<_>>(
+ &"ABCDEFGH".chars().collect::>(),
+ &'A',
+ &'G',
+ successors_wikipedia(),
+ ));
+}
+
+#[library_benchmark]
+fn wikipedia_example_sparse() {
+ check_wikipedia_result(edmonds_karp::<_, _, _, SparseCapacity<_>>(
+ &"ABCDEFGH".chars().collect::>(),
+ &'A',
+ &'G',
+ successors_wikipedia(),
+ ));
+}
+
+library_benchmark_group!(
+ name = edmondskarp;
+ benchmarks = wikipedia_example_dense, wikipedia_example_sparse
+);
+
+main!(library_benchmark_groups = edmondskarp);
diff --git a/benches/iai_kuhn_munkres.rs b/benches/iai_kuhn_munkres.rs
new file mode 100644
index 00000000..a8c69b85
--- /dev/null
+++ b/benches/iai_kuhn_munkres.rs
@@ -0,0 +1,54 @@
+use iai_callgrind::{library_benchmark, library_benchmark_group, main};
+use pathfinding::prelude::{Matrix, kuhn_munkres};
+use rand::{Rng as _, SeedableRng as _};
+use rand_xorshift::XorShiftRng;
+
+const RNG_SEED: [u8; 16] = [
+ 3, 42, 93, 129, 1, 85, 72, 42, 84, 23, 95, 212, 253, 10, 4, 2,
+];
+
+#[library_benchmark]
+fn kuhn_munkres_size_32() {
+ let size = 32;
+ let mut rng = XorShiftRng::from_seed(RNG_SEED);
+ let weights = Matrix::square_from_vec(
+ (0..(size * size))
+ .map(|_| rng.random_range(1..=100))
+ .collect::>(),
+ )
+ .unwrap();
+ kuhn_munkres(&weights);
+}
+
+#[library_benchmark]
+fn kuhn_munkres_size_64() {
+ let size = 64;
+ let mut rng = XorShiftRng::from_seed(RNG_SEED);
+ let weights = Matrix::square_from_vec(
+ (0..(size * size))
+ .map(|_| rng.random_range(1..=100))
+ .collect::>(),
+ )
+ .unwrap();
+ kuhn_munkres(&weights);
+}
+
+#[library_benchmark]
+fn kuhn_munkres_size_128() {
+ let size = 128;
+ let mut rng = XorShiftRng::from_seed(RNG_SEED);
+ let weights = Matrix::square_from_vec(
+ (0..(size * size))
+ .map(|_| rng.random_range(1..=100))
+ .collect::>(),
+ )
+ .unwrap();
+ kuhn_munkres(&weights);
+}
+
+library_benchmark_group!(
+ name = kuhn_munkres_benches;
+ benchmarks = kuhn_munkres_size_32, kuhn_munkres_size_64, kuhn_munkres_size_128
+);
+
+main!(library_benchmark_groups = kuhn_munkres_benches);
diff --git a/benches/iai_separate_components.rs b/benches/iai_separate_components.rs
new file mode 100644
index 00000000..f55a88c6
--- /dev/null
+++ b/benches/iai_separate_components.rs
@@ -0,0 +1,97 @@
+use iai_callgrind::{library_benchmark, library_benchmark_group, main};
+use itertools::Itertools;
+use pathfinding::prelude::separate_components;
+use rand::{Rng as _, RngCore as _, SeedableRng as _, prelude::SliceRandom};
+use rand_xorshift::XorShiftRng;
+use std::collections::HashSet;
+
+const RNG_SEED: [u8; 16] = [
+ 3, 42, 93, 129, 1, 85, 72, 42, 84, 23, 95, 212, 253, 10, 4, 2,
+];
+
+fn larger_separate_components() {
+ // Create 100 groups of 100 elements, then randomly split
+ // into sub-groups.
+ let mut rng = XorShiftRng::from_seed(RNG_SEED);
+ let mut seen = HashSet::new();
+ let mut components = (0..100)
+ .map(|_| {
+ let mut component = Vec::new();
+ for _ in 0..100 {
+ let node = rng.next_u64();
+ if !seen.contains(&node) {
+ seen.insert(node);
+ component.push(node);
+ }
+ }
+ component.sort_unstable();
+ assert!(
+ !component.is_empty(),
+ "component is empty, rng seed needs changing"
+ );
+ component
+ })
+ .collect_vec();
+ components.sort_unstable_by_key(|c| c[0]);
+ let mut groups = components
+ .iter()
+ .flat_map(|component| {
+ let mut component = component.clone();
+ component.shuffle(&mut rng);
+ let mut subcomponents = Vec::new();
+ while !component.is_empty() {
+ let cut = rng.random_range(0..component.len());
+ let mut subcomponent = component.drain(cut..).collect_vec();
+ if !component.is_empty() {
+ subcomponent.push(component[0]);
+ }
+ subcomponent.shuffle(&mut rng);
+ subcomponents.push(subcomponent);
+ }
+ subcomponents
+ })
+ .collect_vec();
+ groups.shuffle(&mut rng);
+ let (_, group_mappings) = separate_components(&groups);
+ let mut out_groups = vec![HashSet::new(); groups.len()];
+ for (i, n) in group_mappings.into_iter().enumerate() {
+ assert!(
+ n < groups.len(),
+ "group index is greater than expected: {}/{}",
+ n,
+ groups.len()
+ );
+ for e in &groups[i] {
+ out_groups[n].insert(*e);
+ }
+ }
+ let out_groups = out_groups
+ .into_iter()
+ .map(|g| g.into_iter().collect_vec())
+ .collect_vec();
+ let mut out_groups = out_groups
+ .into_iter()
+ .filter_map(|mut group| {
+ if group.is_empty() {
+ None
+ } else {
+ group.sort_unstable();
+ Some(group)
+ }
+ })
+ .collect_vec();
+ out_groups.sort_by_key(|c| c[0]);
+ assert_eq!(out_groups, components);
+}
+
+#[library_benchmark]
+fn bench_separate_components() {
+ larger_separate_components();
+}
+
+library_benchmark_group!(
+ name = separate_components;
+ benchmarks = bench_separate_components
+);
+
+main!(library_benchmark_groups = separate_components);