Skip to content

Commit d381a47

Browse files
committed
Changed: Optimizes flip algorithm for improved performance
Refactors the flip algorithm to use more efficient data structures (SmallBuffer instead of FastHashMap/Vec) and hashing techniques (hash-only dedup) to improve performance. Also refactors benchmark seed search to avoid `std::process::exit`. These changes significantly reduce the time spent in the flip algorithm, leading to faster triangulation construction. (internal) Refs: perf/optimize-flips
1 parent 8890222 commit d381a47

8 files changed

Lines changed: 113 additions & 81 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@ derive_builder = "0.20.2"
2222
la-stack = "0.1.3"
2323
tracing = "0.1.44"
2424
rustc-hash = "2.1.1" # Fast non-cryptographic hashing for performance
25-
smallvec = { version = "1.15.1", features = [
26-
"serde",
27-
] } # Stack allocation for small collections
25+
smallvec = { version = "1.15.1", features = [ "serde" ] } # Stack allocation for small collections
2826
num-traits = "0.2.19"
2927
ordered-float = { version = "5.1.0", features = [ "serde" ] }
3028
rand = "0.10.0"
3129
serde = { version = "1.0.228", features = [ "derive" ] }
3230
slotmap = { version = "1.1.1", features = [ "serde" ] }
3331
thiserror = "2.0.18"
34-
uuid = { version = "1.20.0", features = [ "v4", "serde" ] }
32+
uuid = { version = "1.20.0", features = [ "v4", "serde", "fast-rng" ] }
3533

3634
[dev-dependencies]
3735
approx = "0.5.1"

benches/ci_performance_suite.rs

Lines changed: 83 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@
2525
//! - 3D-5D: Higher-dimensional triangulations as documented in README.md
2626
2727
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
28-
use delaunay::core::delaunay_triangulation::{ConstructionOptions, RetryPolicy};
2928
use delaunay::geometry::util::generate_random_points_seeded;
30-
use delaunay::prelude::DelaunayTriangulation;
29+
use delaunay::prelude::{ConstructionOptions, DelaunayTriangulation, RetryPolicy};
3130
use delaunay::vertex;
3231
use std::hint::black_box;
3332
use std::num::NonZeroUsize;
@@ -70,6 +69,88 @@ macro_rules! benchmark_tds_new_dimension {
7069
)]
7170
fn $func_name(c: &mut Criterion) {
7271
let counts = COUNTS;
72+
73+
// Opt-in helper for discovering stable seeds without paying Criterion warmup/
74+
// measurement cost per seed.
75+
//
76+
// NOTE: This helper is intentionally per (dim, count) benchmark case.
77+
// It returns early on the first successful seed (and panics on failure),
78+
// so it is meant to be run with a Criterion filter that selects a single
79+
// case, for example:
80+
//
81+
// cargo bench --bench ci_performance_suite -- 'tds_new_3d/tds_new/50'
82+
//
83+
// Because the base seed is derived from `count`, a seed that works for one
84+
// count may still fail for a different count.
85+
//
86+
// We avoid `std::process::exit` here so that destructors run and Criterion
87+
// can clean up state on both success and failure.
88+
if bench_seed_search_enabled() {
89+
let bounds = (-100.0, 100.0);
90+
let filters: Vec<String> = std::env::args()
91+
.skip(1)
92+
.filter(|arg| !arg.starts_with('-'))
93+
.collect();
94+
95+
for &count in counts {
96+
let bench_id =
97+
format!("tds_new_{}d/tds_new/{}", stringify!($dim), count);
98+
99+
if !filters.is_empty() && !filters.iter().any(|filter| bench_id.contains(filter)) {
100+
continue;
101+
}
102+
103+
let seed = ($seed as u64).wrapping_add(count as u64);
104+
let limit = bench_seed_search_limit();
105+
106+
for offset in 0..limit {
107+
let candidate_seed = seed.wrapping_add(offset as u64);
108+
let points = generate_random_points_seeded::<f64, $dim>(
109+
count,
110+
bounds,
111+
candidate_seed,
112+
)
113+
.expect(concat!(
114+
"generate_random_points_seeded failed for ",
115+
stringify!($dim),
116+
"D"
117+
));
118+
let vertices = points.iter().map(|p| vertex!(*p)).collect::<Vec<_>>();
119+
120+
let options =
121+
ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled {
122+
attempts: NonZeroUsize::new(6)
123+
.expect("retry attempts must be non-zero"),
124+
base_seed: Some(candidate_seed),
125+
});
126+
127+
if DelaunayTriangulation::<_, (), (), $dim>::new_with_options(
128+
&vertices,
129+
options,
130+
)
131+
.is_ok()
132+
{
133+
println!(
134+
"seed_search_found dim={} count={} seed={}",
135+
$dim, count, candidate_seed
136+
);
137+
return;
138+
}
139+
}
140+
141+
panic!(
142+
"seed_search_failed dim={} count={} start_seed={} limit={}",
143+
$dim,
144+
count,
145+
seed,
146+
limit
147+
);
148+
}
149+
150+
// No filter matched this benchmark function; do nothing.
151+
return;
152+
}
153+
73154
let mut group = c.benchmark_group(concat!("tds_new_", stringify!($dim), "d"));
74155

75156
// Set smaller sample sizes for higher dimensions to keep CI times reasonable
@@ -92,65 +173,6 @@ macro_rules! benchmark_tds_new_dimension {
92173
let bounds = (-100.0, 100.0);
93174
let seed = ($seed as u64).wrapping_add(count as u64);
94175

95-
// Opt-in helper for discovering stable seeds without paying Criterion warmup/
96-
// measurement cost per seed.
97-
//
98-
// NOTE: This helper is intentionally per (dim, count) benchmark case.
99-
// It `exit(0)`s on the first successful seed (and `exit(1)`s on failure),
100-
// so it is meant to be run with a Criterion filter that selects a single
101-
// case, for example:
102-
//
103-
// cargo bench --bench ci_performance_suite -- 'tds_new_3d/tds_new/50'
104-
//
105-
// Because the base seed is derived from `count`, a seed that works for one
106-
// count may still fail for a different count.
107-
if bench_seed_search_enabled() {
108-
let limit = bench_seed_search_limit();
109-
for offset in 0..limit {
110-
let candidate_seed = seed.wrapping_add(offset as u64);
111-
let points = generate_random_points_seeded::<f64, $dim>(
112-
count,
113-
bounds,
114-
candidate_seed,
115-
)
116-
.expect(concat!(
117-
"generate_random_points_seeded failed for ",
118-
stringify!($dim),
119-
"D"
120-
));
121-
let vertices = points.iter().map(|p| vertex!(*p)).collect::<Vec<_>>();
122-
123-
let options =
124-
ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled {
125-
attempts: NonZeroUsize::new(6)
126-
.expect("retry attempts must be non-zero"),
127-
base_seed: Some(candidate_seed),
128-
});
129-
130-
if DelaunayTriangulation::<_, (), (), $dim>::new_with_options(
131-
&vertices,
132-
options,
133-
)
134-
.is_ok()
135-
{
136-
println!(
137-
"seed_search_found dim={} count={} seed={}",
138-
$dim, count, candidate_seed
139-
);
140-
std::process::exit(0);
141-
}
142-
}
143-
144-
println!(
145-
"seed_search_failed dim={} count={} start_seed={} limit={}",
146-
$dim,
147-
count,
148-
seed,
149-
limit
150-
);
151-
std::process::exit(1);
152-
}
153-
154176
let points = generate_random_points_seeded::<f64, $dim>(count, bounds, seed)
155177
.expect(concat!(
156178
"generate_random_points_seeded failed for ",

src/core/algorithms/flips.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,8 +1700,10 @@ where
17001700
return Err(FlipError::InvalidRidgeMultiplicity { found: cells.len() });
17011701
}
17021702

1703-
let mut opposite_counts: FastHashMap<VertexKey, usize> = FastHashMap::default();
1704-
let mut extras_per_cell: Vec<[VertexKey; 2]> = Vec::with_capacity(3);
1703+
// k=3 flip contexts are tiny (exactly 3 cells, with 2 "extra" vertices per cell).
1704+
// Use flat buffers + linear counting to avoid HashMap/Vec overhead in this hot path.
1705+
let mut opposite_counts: SmallBuffer<(VertexKey, u8), 3> = SmallBuffer::new();
1706+
let mut extras_per_cell: SmallBuffer<[VertexKey; 2], 3> = SmallBuffer::new();
17051707

17061708
for &ck in &cells {
17071709
let cell = tds
@@ -1712,22 +1714,28 @@ where
17121714
return Err(FlipError::InvalidRidgeAdjacency { cell_key: ck });
17131715
}
17141716

1715-
for &v in &extras {
1716-
*opposite_counts.entry(v).or_insert(0) += 1;
1717-
}
17181717
let extras_pair: [VertexKey; 2] = extras
17191718
.as_slice()
17201719
.try_into()
17211720
.map_err(|_| FlipError::InvalidRidgeAdjacency { cell_key: ck })?;
1721+
1722+
for &v in &extras_pair {
1723+
if let Some((_key, count)) = opposite_counts.iter_mut().find(|(key, _)| *key == v) {
1724+
*count += 1;
1725+
} else {
1726+
opposite_counts.push((v, 1));
1727+
}
1728+
}
1729+
17221730
extras_per_cell.push(extras_pair);
17231731
}
17241732

1725-
if opposite_counts.len() != 3 || !opposite_counts.values().all(|&count| count == 2) {
1733+
if opposite_counts.len() != 3 || !opposite_counts.iter().all(|(_v, count)| *count == 2) {
17261734
return Err(FlipError::InvalidRidgeAdjacency { cell_key });
17271735
}
17281736

17291737
let mut opposite_vertices: SmallBuffer<VertexKey, 3> =
1730-
opposite_counts.keys().copied().collect();
1738+
opposite_counts.iter().map(|(v, _count)| *v).collect();
17311739
opposite_vertices.sort_unstable();
17321740
let opposite_vertices: [VertexKey; 3] = opposite_vertices
17331741
.as_slice()
@@ -3801,6 +3809,8 @@ where
38013809
continue;
38023810
}
38033811

3812+
// Intentional hash-only dedup (no vertex-level tie-break): a 64-bit collision is
3813+
// astronomically unlikely, and avoiding extra comparisons keeps this hot path fast.
38043814
if candidate_facet_info
38053815
.iter()
38063816
.any(|(hash, _info)| *hash == facet_hash)
@@ -3871,6 +3881,7 @@ where
38713881
}
38723882
let facet_hash = stable_hash_u64_slice(&facet_values);
38733883

3884+
// Hash-only lookup (see comment above); collision risk is astronomically low.
38743885
let Ok(idx) =
38753886
candidate_facet_info.binary_search_by_key(&facet_hash, |(hash, _info)| *hash)
38763887
else {

src/geometry/matrix.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,11 @@ pub(crate) fn matrix_set<const D: usize>(m: &mut Matrix<D>, r: usize, c: usize,
196196
/// use delaunay::geometry::matrix::{determinant, Matrix};
197197
///
198198
/// let m = Matrix::<2>::zero();
199-
/// assert_eq!(determinant(m), 0.0);
199+
/// assert_eq!(determinant(&m), 0.0);
200200
/// ```
201201
#[inline]
202202
#[must_use]
203-
pub fn determinant<const D: usize>(m: Matrix<D>) -> f64 {
203+
pub fn determinant<const D: usize>(m: &Matrix<D>) -> f64 {
204204
match m.det(0.0) {
205205
Ok(det) => det,
206206
Err(LaError::Singular { .. }) => 0.0,

src/geometry/predicates.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ where
163163
let tolerance_f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol);
164164

165165
// Calculate determinant (singular => 0; non-finite => NaN).
166-
let det = determinant(matrix);
166+
let det = determinant(&matrix);
167167

168168
if det > tolerance_f64 {
169169
Ok(Orientation::POSITIVE)
@@ -423,7 +423,7 @@ where
423423
let base_tol = safe_scalar_to_f64(T::default_tolerance())?;
424424
let tolerance_f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol);
425425

426-
let det = determinant(matrix);
426+
let det = determinant(&matrix);
427427
let orientation = simplex_orientation(simplex_points)?;
428428

429429
match orientation {
@@ -626,7 +626,7 @@ where
626626
let tolerance_f64: f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol);
627627

628628
// Calculate determinant (singular => 0; non-finite => NaN).
629-
let det = determinant(matrix);
629+
let det = determinant(&matrix);
630630

631631
// The sign interpretation depends on both orientation and dimension parity
632632
// For the lifted matrix formulation, even and odd dimensions have opposite sign conventions

src/geometry/robust_predicates.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ where
282282
fill_insphere_predicate_matrix(&mut matrix, simplex_points, test_point)?;
283283

284284
let tol_f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol);
285-
let det = determinant(matrix);
285+
let det = determinant(&matrix);
286286

287287
Ok::<(f64, f64), CoordinateConversionError>((det, tol_f64))
288288
})?;
@@ -342,7 +342,7 @@ where
342342
}
343343

344344
// Determinant with scale correction.
345-
let det = determinant(matrix) * scale_factor;
345+
let det = determinant(&matrix) * scale_factor;
346346

347347
Ok::<(f64, f64), CoordinateConversionError>((det, tolerance_raw))
348348
})?;
@@ -445,7 +445,7 @@ where
445445
let tolerance_f64: f64 = crate::geometry::matrix::adaptive_tolerance(&matrix, base_tol);
446446

447447
// Calculate determinant (singular => 0; non-finite => NaN).
448-
let det = determinant(matrix);
448+
let det = determinant(&matrix);
449449

450450
if det > tolerance_f64 {
451451
Ok(Orientation::POSITIVE)

tests/circumsphere_debug_tools.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ fn build_and_analyze_matrix(simplex_vertices: &[Vertex<f64, i32, 3>]) -> (f64, b
963963
);
964964
}
965965

966-
let det = determinant(matrix);
966+
let det = determinant(&matrix);
967967
println!();
968968
println!("Determinant: {det:.6}");
969969

0 commit comments

Comments
 (0)