diff --git a/CHANGELOG.md b/CHANGELOG.md index a970a03..4bd77d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.2.2] - 2025-11-09 +- performance atan2 +- parformance unstable sort (total 64%) + ## [0.2.0] - 2025-11-09 - Error enum for returning the exact cause. - Added test/benchmark data generators. diff --git a/Cargo.lock b/Cargo.lock index ece5404..4e54396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "nfp" -version = "0.2.1" +version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 091a8ff..e1b9232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nfp" -version = "0.2.1" +version = "0.2.2" edition = "2024" description = "No Fit Polygon" categories = ["science::geo", "data-structures", "game-development", "graphics"] diff --git a/benches/nfp_bench100.rs b/benches/nfp_bench100.rs index 42c6c07..c1592c2 100644 --- a/benches/nfp_bench100.rs +++ b/benches/nfp_bench100.rs @@ -33,7 +33,7 @@ fn main() { println!("\nResults:"); println!("--------"); println!("Total time: {:?}", elapsed); - println!("Time per run: {:.3} ms", elapsed.as_secs_f64() * 1000.0 / ITERATIONS as f64); + println!("Time per run: {:.3} μs", elapsed.as_secs_f64() * 1_000_000.0 / ITERATIONS as f64); println!("Runs per sec: {:.0}", ITERATIONS as f64 / elapsed.as_secs_f64()); } @@ -82,8 +82,7 @@ fn compute_centroid(points: &[Point]) -> Point { } /* -cargo bench --bench nfp_benchmark -samply record cargo run --release --example perf_build +cargo bench --bench nfp_bench100 Iterations: 1000 @@ -99,5 +98,16 @@ Total time: 4.152361987s Time per run: 4.152 ms Runs per sec: 241 _______________________________________________ +Opt 1 ditch atan2 +Total time: 1.725334029s +Time per run: 1725.334 μs +Runs per sec: 580 +_______________________________________________ +Opt2 unstable sorting + +Total time: 1.44724319s +Time per run: 1447.243 μs +Runs per sec: 691 +_______________________________________________ */ diff --git a/benches/nfp_bench200.rs b/benches/nfp_bench200.rs index a92bc7c..d7ca2bf 100644 --- a/benches/nfp_bench200.rs +++ b/benches/nfp_bench200.rs @@ -33,7 +33,7 @@ fn main() { println!("\nResults:"); println!("--------"); println!("Total time: {:?}", elapsed); - println!("Time per run: {:.3} ms", elapsed.as_secs_f64() * 1000.0 / ITERATIONS as f64); + println!("Time per run: {:.3} μs", elapsed.as_secs_f64() * 1_000_000.0 / ITERATIONS as f64); println!("Runs per sec: {:.0}", ITERATIONS as f64 / elapsed.as_secs_f64()); } @@ -82,8 +82,7 @@ fn compute_centroid(points: &[Point]) -> Point { } /* -cargo bench --bench nfp_benchmark - +cargo bench --bench nfp_bench200 Iterations: 300 @@ -99,5 +98,16 @@ Total time: 5.604518697s Time per run: 18.682 ms Runs per sec: 54 _______________________________________________ +Opt 1 ditch atan2 + +Total time: 2.456991368s +Time per run: 8189.971 μs +Runs per sec: 122 +_______________________________________________ +Opt2 unstable sorting +Total time: 2.027956028s +Time per run: 6759.853 μs +Runs per sec: 148 +_______________________________________________ */ diff --git a/benches/nfp_bench500.rs b/benches/nfp_bench500.rs index a0e8c2c..ee00911 100644 --- a/benches/nfp_bench500.rs +++ b/benches/nfp_bench500.rs @@ -33,7 +33,7 @@ fn main() { println!("\nResults:"); println!("--------"); println!("Total time: {:?}", elapsed); - println!("Time per run: {:.3} ms", elapsed.as_secs_f64() * 1000.0 / ITERATIONS as f64); + println!("Time per run: {:.3} μs", elapsed.as_secs_f64() * 1_000_000.0 / ITERATIONS as f64); println!("Runs per sec: {:.0}", ITERATIONS as f64 / elapsed.as_secs_f64()); } @@ -82,8 +82,7 @@ fn compute_centroid(points: &[Point]) -> Point { } /* -cargo bench --bench nfp_benchmark - +cargo bench --bench nfp_bench500 Iterations: 100 @@ -99,5 +98,16 @@ Total time: 13.578851406s Time per run: 135.789 ms Runs per sec: 7 _______________________________________________ +Opt 1 ditch atan2 + +Total time: 6.140549308s +Time per run: 61405.493 μs +Runs per sec: 16 +_______________________________________________ +Opt2 unstable sorting +Total time: 4.848619896s +Time per run: 48486.199 μs +Runs per sec: 21 +_______________________________________________ */ \ No newline at end of file diff --git a/src/nfp_points.rs b/src/nfp_points.rs index 6ad8359..bfe8dd3 100644 --- a/src/nfp_points.rs +++ b/src/nfp_points.rs @@ -157,7 +157,7 @@ impl NFP { // Remove near-duplicate points (within tolerance) let tolerance = 1e-10; - nfp_vertices.sort_by(|a, b| { + nfp_vertices.sort_unstable_by(|a, b| { a.x.partial_cmp(&b.x) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal)) @@ -166,13 +166,9 @@ impl NFP { (a.x - b.x).abs() < tolerance && (a.y - b.y).abs() < tolerance }); - // Sort vertices in CCW order by angle from centroid + // Sort vertices in CCW order by angle from centroid (avoid atan2) let centroid = compute_centroid(&nfp_vertices); - nfp_vertices.sort_by(|a, b| { - let angle_a = (a.y - centroid.y).atan2(a.x - centroid.x); - let angle_b = (b.y - centroid.y).atan2(b.x - centroid.x); - angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal) - }); + nfp_vertices.sort_unstable_by(|a, b| angle_cmp(a, b, ¢roid)); Ok(nfp_vertices) } @@ -190,6 +186,39 @@ fn compute_centroid(points: &[Point]) -> Point { Point::new(sum_x / len, sum_y / len) } +/// Tolerance for comparing collinear points in angle sorting +const ANGLE_CMP_EPSILON: f64 = 1e-10; + +// Compare points by angle around a given `centroid` without using `atan2`. +// Uses half-plane test and perp (2D cross product) to determine CCW ordering. +fn angle_cmp(a: &Point, b: &Point, centroid: &Point) -> std::cmp::Ordering { + use std::cmp::Ordering; + + let ax = a.x - centroid.x; + let ay = a.y - centroid.y; + let bx = b.x - centroid.x; + let by = b.y - centroid.y; + + // Place points into two half-planes: upper (y>0 or y==0 && x>=0) and lower. + let a_up = (ay > 0.0) || (ay == 0.0 && ax >= 0.0); + let b_up = (by > 0.0) || (by == 0.0 && bx >= 0.0); + if a_up != b_up { + // a_up true should come before b_up false + return a_up.cmp(&b_up).reverse(); + } + + // Same half-plane: use perp (2D cross product) to determine order + let perp = ax * by - ay * bx; + if perp.abs() > ANGLE_CMP_EPSILON { + return if perp > 0.0 { Ordering::Less } else { Ordering::Greater }; + } + + // Collinear: sort by distance from centroid (closer first) + let da = ax * ax + ay * ay; + let db = bx * bx + by * by; + da.partial_cmp(&db).unwrap_or(Ordering::Equal) +} + #[cfg(test)] mod tests { use super::*;