From 65f102e7f35c9bf3309c5c83a4c953a4fdf4c00a Mon Sep 17 00:00:00 2001 From: tachsin Date: Sat, 12 Sep 2026 10:35:42 +0300 Subject: [PATCH] bench: make the iai benchmarks measure the work they run Seven of the fourteen iai_algos benchmarks report on the order of a hundred instructions for searches that execute hundreds of thousands of basic blocks. Callgrind only counts while collection is toggled on around the benchmark function, and a search that is inlined into the harness and folded away falls outside that window. Because it depends on inlining, the affected set moves with unrelated edits, so the base-versus-PR comparison in the workflow can report an enormous change for a PR that did nothing of the sort. Run each search in an #[inline(never)] helper and return its result, so the optimiser cannot see through the call and cannot discard the result. All fourteen now measure, and they keep measuring across changes to the algorithms themselves. The workloads are unchanged. Fixes #833 --- benches/iai_algos.rs | 239 +++++++++++++++++++++++++------------------ 1 file changed, 138 insertions(+), 101 deletions(-) diff --git a/benches/iai_algos.rs b/benches/iai_algos.rs index 49dac531..9e52c9ad 100644 --- a/benches/iai_algos.rs +++ b/benches/iai_algos.rs @@ -1,5 +1,16 @@ +//! Instruction-count benchmarks for the search algorithms. +//! +//! Each search runs inside an `#[inline(never)]` helper whose result is returned from the +//! benchmark. Both halves matter. Callgrind only counts instructions while collection is +//! toggled on around the benchmark function, so a search that gets inlined into the harness +//! and folded away is reported as a few hundred instructions rather than a few million; and +//! because that depends on inlining, it changes with unrelated edits and turns into a +//! spurious regression in the CI comparison. Keeping the work behind a call the optimiser +//! cannot see through, and consuming the result, keeps the measurement honest. + use iai_callgrind::{library_benchmark, library_benchmark_group, main}; use pathfinding::prelude::{astar, bfs, bfs_bidirectional, dfs, dijkstra, fringe, idastar, iddfs}; +use std::hint::black_box; #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] struct Pt { @@ -36,148 +47,174 @@ fn successors(pt: &Pt) -> Vec { ret } +#[inline] +fn weighted(pt: &Pt) -> impl Iterator + use<> { + successors(pt).into_iter().map(|n| (n, 1)) +} + +const fn at_corner(n: &Pt) -> bool { + n.x == 64 && n.y == 64 +} + +const fn never(_: &Pt) -> bool { + false +} + +const fn at_five(n: &Pt) -> bool { + n.x == 5 && n.y == 5 +} + +const fn one(_: &Pt) -> usize { + 1 +} + +type Path = Option<(Vec, usize)>; + +#[inline(never)] +fn run_astar(start: &Pt, success: fn(&Pt) -> bool, heuristic: fn(&Pt) -> usize) -> Path { + astar(start, weighted, heuristic, success) +} + +#[inline(never)] +fn run_dijkstra(start: &Pt, success: fn(&Pt) -> bool) -> Path { + dijkstra(start, weighted, success) +} + +#[inline(never)] +fn run_fringe(start: &Pt, success: fn(&Pt) -> bool, heuristic: fn(&Pt) -> usize) -> Path { + fringe(start, weighted, heuristic, success) +} + +#[inline(never)] +fn run_idastar(start: &Pt, success: fn(&Pt) -> bool) -> Path { + idastar(start, weighted, Pt::heuristic, success) +} + +#[inline(never)] +fn run_bfs(start: &Pt, success: fn(&Pt) -> bool) -> Option> { + bfs(start, successors, success) +} + +#[inline(never)] +fn run_dfs(start: Pt, success: fn(&Pt) -> bool) -> Option> { + dfs(start, successors, success) +} + +#[inline(never)] +fn run_iddfs(start: Pt, success: fn(&Pt) -> bool) -> Option> { + iddfs(start, successors, success) +} + +#[inline(never)] +fn run_bfs_bidirectional(start: &Pt, end: &Pt) -> Option> { + bfs_bidirectional(start, end, successors, successors) +} + +/// The no-path case searches backwards from an unreachable node that has no successors. +#[inline(never)] +fn run_bfs_bidirectional_no_path(start: &Pt, end: &Pt) -> Option> { + bfs_bidirectional(start, end, successors, |_| vec![]) +} + #[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 - ); +fn corner_to_corner_astar() -> Path { + let path = run_astar(&black_box(Pt::new(0, 0)), at_corner, Pt::heuristic); + assert!(path.is_some()); + path } #[library_benchmark] -fn corner_to_corner_bfs() { - assert_ne!( - bfs(&Pt::new(0, 0), successors, |n| n.x == 64 && n.y == 64), - None - ); +fn corner_to_corner_bfs() -> Option> { + let path = run_bfs(&black_box(Pt::new(0, 0)), at_corner); + assert!(path.is_some()); + path } #[library_benchmark] -fn corner_to_corner_bfs_bidirectional() { - assert_ne!( - bfs_bidirectional(&Pt::new(0, 0), &Pt::new(64, 64), successors, successors), - None - ); +fn corner_to_corner_bfs_bidirectional() -> Option> { + let path = run_bfs_bidirectional(&black_box(Pt::new(0, 0)), &black_box(Pt::new(64, 64))); + assert!(path.is_some()); + path } #[library_benchmark] -fn corner_to_corner_dfs() { - assert_ne!( - dfs(Pt::new(0, 0), successors, |n| n.x == 64 && n.y == 64), - None - ); +fn corner_to_corner_dfs() -> Option> { + let path = run_dfs(black_box(Pt::new(0, 0)), at_corner); + assert!(path.is_some()); + path } #[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 - ); +fn corner_to_corner_dijkstra() -> Path { + let path = run_dijkstra(&black_box(Pt::new(0, 0)), at_corner); + assert!(path.is_some()); + path } #[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 - ); +fn corner_to_corner_fringe() -> Path { + let path = run_fringe(&black_box(Pt::new(0, 0)), at_corner, Pt::heuristic); + assert!(path.is_some()); + path } #[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 - ); +fn corner_to_corner_idastar() -> Path { + let path = run_idastar(&black_box(Pt::new(0, 0)), at_corner); + assert!(path.is_some()); + path } #[library_benchmark] -fn corner_to_corner_iddfs() { - assert_ne!( - iddfs(Pt::new(0, 0), successors, |n| n.x == 5 && n.y == 5), - None - ); +fn corner_to_corner_iddfs() -> Option> { + let path = run_iddfs(black_box(Pt::new(0, 0)), at_five); + assert!(path.is_some()); + path } #[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 - ); +fn no_path_astar() -> Path { + let path = run_astar(&black_box(Pt::new(2, 3)), never, one); + assert!(path.is_none()); + path } #[library_benchmark] -fn no_path_bfs() { - assert_eq!(bfs(&Pt::new(2, 3), successors, |_| false), None); +fn no_path_bfs() -> Option> { + let path = run_bfs(&black_box(Pt::new(2, 3)), never); + assert!(path.is_none()); + path } #[library_benchmark] -fn no_path_bfs_bidirectional() { - assert_eq!( - bfs_bidirectional( - &Pt::new(2, 3), - &Pt::new(u16::MAX, u16::MAX), - successors, - |_| vec![] - ), - None +fn no_path_bfs_bidirectional() -> Option> { + let path = run_bfs_bidirectional_no_path( + &black_box(Pt::new(2, 3)), + &black_box(Pt::new(u16::MAX, u16::MAX)), ); + assert!(path.is_none()); + path } #[library_benchmark] -fn no_path_dfs() { - assert_eq!(dfs(Pt::new(2, 3), successors, |_| false), None); +fn no_path_dfs() -> Option> { + let path = run_dfs(black_box(Pt::new(2, 3)), never); + assert!(path.is_none()); + path } #[library_benchmark] -fn no_path_dijkstra() { - assert_eq!( - dijkstra( - &Pt::new(2, 3), - |n| successors(n).into_iter().map(|n| (n, 1)), - |_| false, - ), - None - ); +fn no_path_dijkstra() -> Path { + let path = run_dijkstra(&black_box(Pt::new(2, 3)), never); + assert!(path.is_none()); + path } #[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 - ); +fn no_path_fringe() -> Path { + let path = run_fringe(&black_box(Pt::new(2, 3)), never, one); + assert!(path.is_none()); + path } library_benchmark_group!(