Skip to content

Commit 1e394cb

Browse files
committed
parallel benchmark, merged legacy queries
1 parent c3ab812 commit 1e394cb

File tree

7 files changed

+851
-1306
lines changed

7 files changed

+851
-1306
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ harness = false
4141
name = "profile_bench"
4242
harness = false
4343

44+
[[bench]]
45+
name = "profile_parallel"
46+
harness = false
47+
4448
[lints.rust]
4549
keyword_idents_2024 = "forbid"
4650
non_ascii_idents = "forbid"

benches/profile_bench.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ fn main() {
8282
let mut results = Vec::new();
8383
let query_start = Instant::now();
8484
for (min_x, min_y, max_x, max_y) in &test_queries_small {
85-
results.clear();
8685
tree.query_intersecting(*min_x, *min_y, *max_x, *max_y, &mut results);
8786
}
8887
let small_query_time = query_start.elapsed();
@@ -94,10 +93,8 @@ fn main() {
9493
);
9594

9695
// Large queries
97-
results.clear();
9896
let query_start = Instant::now();
9997
for (min_x, min_y, max_x, max_y) in &test_queries_large {
100-
results.clear();
10198
tree.query_intersecting(*min_x, *min_y, *max_x, *max_y, &mut results);
10299
}
103100
let large_query_time = query_start.elapsed();
@@ -120,7 +117,6 @@ fn main() {
120117

121118
let query_start = Instant::now();
122119
for i in 0..num_queries {
123-
results.clear();
124120
let x = coords[4 * i];
125121
let y = coords[4 * i + 1];
126122
tree.query_nearest_k(x, y, k, &mut results);

benches/profile_parallel.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
//! Parallel query benchmark to measure concurrent access performance
2+
//!
3+
//! This benchmark uses the SAME queries as profile_bench.rs so results are directly comparable.
4+
//! It demonstrates that the HilbertRTree is safe to share across threads without interior
5+
//! mutability, since queries only require &self (immutable borrow).
6+
7+
use aabb::HilbertRTree;
8+
use rand::Rng;
9+
use rand::SeedableRng;
10+
use std::sync::Arc;
11+
use std::thread;
12+
use std::time::Instant;
13+
14+
fn main() {
15+
println!("AABB Parallel Query Benchmark (vs profile_bench.rs)");
16+
println!("===================================================\n");
17+
18+
let num_items = 1_000_000;
19+
let num_tests = 1_000;
20+
let num_threads = 10;
21+
22+
// Create MT19937 RNG with fixed seed for reproducibility (SAME as profile_bench.rs)
23+
let seed = 95756739_u64;
24+
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
25+
26+
// Generate random boxes for indexing (coordinate space: 100x100)
27+
let mut coords = Vec::new();
28+
println!("Generating {} random boxes...", num_items);
29+
let gen_start = Instant::now();
30+
for _ in 0..num_items {
31+
let min_x = rng.random_range(0.0..100.0);
32+
let min_y = rng.random_range(0.0..100.0);
33+
let max_x = (min_x + rng.random_range(0.0..1.0_f64)).min(100.0);
34+
let max_y = (min_y + rng.random_range(0.0..1.0_f64)).min(100.0);
35+
36+
coords.push(min_x);
37+
coords.push(min_y);
38+
coords.push(max_x);
39+
coords.push(max_y);
40+
}
41+
let gen_time = gen_start.elapsed();
42+
println!(" Generated in {:.2}ms\n", gen_time.as_secs_f64() * 1000.0);
43+
44+
// Build index (SAME as profile_bench.rs)
45+
println!("Building index...");
46+
let build_start = Instant::now();
47+
let mut tree = HilbertRTree::with_capacity(num_items);
48+
49+
for chunk in coords.chunks(4) {
50+
if chunk.len() == 4 {
51+
tree.add(chunk[0], chunk[1], chunk[2], chunk[3]);
52+
}
53+
}
54+
tree.build();
55+
let build_time = build_start.elapsed();
56+
println!(" Index built in {:.2}ms\n", build_time.as_secs_f64() * 1000.0);
57+
58+
// Wrap tree in Arc for safe sharing across threads
59+
let tree = Arc::new(tree);
60+
61+
// Generate test queries (SAME as profile_bench.rs)
62+
let mut test_queries_small = Vec::new();
63+
let mut test_queries_large = Vec::new();
64+
65+
for _ in 0..num_tests {
66+
// Small query (0.01% coverage)
67+
let min_x = rng.random_range(0.0..99.0);
68+
let min_y = rng.random_range(0.0..99.0);
69+
test_queries_small.push((min_x, min_y, min_x + 1.0, min_y + 1.0));
70+
71+
// Large query (10% coverage)
72+
let min_x = rng.random_range(0.0..69.0);
73+
let min_y = rng.random_range(0.0..69.0);
74+
test_queries_large.push((min_x, min_y, min_x + 31.62, min_y + 31.62));
75+
}
76+
77+
// Parallel benchmark: Small queries
78+
println!("Profiling query_intersecting (parallel):");
79+
println!("{}", "-".repeat(40));
80+
81+
let queries_small = Arc::new(test_queries_small);
82+
let parallel_start = Instant::now();
83+
84+
let handles: Vec<_> = (0..num_threads)
85+
.map(|_| {
86+
let tree_clone = Arc::clone(&tree);
87+
let queries_clone = Arc::clone(&queries_small);
88+
89+
thread::spawn(move || {
90+
let mut results = Vec::new();
91+
for (min_x, min_y, max_x, max_y) in queries_clone.iter() {
92+
tree_clone.query_intersecting(*min_x, *min_y, *max_x, *max_y, &mut results);
93+
}
94+
})
95+
})
96+
.collect();
97+
98+
for handle in handles {
99+
handle.join().unwrap();
100+
}
101+
102+
let parallel_elapsed = parallel_start.elapsed();
103+
let total_queries = num_threads * num_tests;
104+
println!(
105+
" {} small queries (parallel {}×{}): {:.2}ms ({:.3}µs/query)",
106+
total_queries,
107+
num_threads,
108+
num_tests,
109+
parallel_elapsed.as_secs_f64() * 1000.0,
110+
parallel_elapsed.as_secs_f64() * 1_000_000.0 / total_queries as f64
111+
);
112+
113+
// Parallel benchmark: Large queries
114+
let queries_large = Arc::new(test_queries_large);
115+
let parallel_start = Instant::now();
116+
117+
let handles: Vec<_> = (0..num_threads)
118+
.map(|_| {
119+
let tree_clone = Arc::clone(&tree);
120+
let queries_clone = Arc::clone(&queries_large);
121+
122+
thread::spawn(move || {
123+
let mut results = Vec::new();
124+
for (min_x, min_y, max_x, max_y) in queries_clone.iter() {
125+
tree_clone.query_intersecting(*min_x, *min_y, *max_x, *max_y, &mut results);
126+
}
127+
})
128+
})
129+
.collect();
130+
131+
for handle in handles {
132+
handle.join().unwrap();
133+
}
134+
135+
let parallel_elapsed = parallel_start.elapsed();
136+
println!(
137+
" {} large queries (parallel {}×{}): {:.2}ms ({:.3}µs/query)",
138+
total_queries,
139+
num_threads,
140+
num_tests,
141+
parallel_elapsed.as_secs_f64() * 1000.0,
142+
parallel_elapsed.as_secs_f64() * 1_000_000.0 / total_queries as f64
143+
);
144+
145+
// Parallel benchmark: query_nearest_k
146+
println!("\nProfiling query_nearest_k (parallel):");
147+
println!("{}", "-".repeat(40));
148+
149+
let k_values = vec![1, 10, 100, 1000];
150+
let coords = Arc::new(coords);
151+
152+
for k in k_values {
153+
let num_queries = if k == 1000 { 100 } else { num_tests };
154+
let total_parallel_queries = num_threads * num_queries;
155+
156+
let parallel_start = Instant::now();
157+
158+
let handles: Vec<_> = (0..num_threads)
159+
.map(|thread_id| {
160+
let tree_clone = Arc::clone(&tree);
161+
let coords_clone = Arc::clone(&coords);
162+
163+
thread::spawn(move || {
164+
let mut results = Vec::new();
165+
for i in 0..num_queries {
166+
let idx = (thread_id * num_queries + i) % (coords_clone.len() / 4);
167+
let x = coords_clone[4 * idx];
168+
let y = coords_clone[4 * idx + 1];
169+
tree_clone.query_nearest_k(x, y, k, &mut results);
170+
}
171+
})
172+
})
173+
.collect();
174+
175+
for handle in handles {
176+
handle.join().unwrap();
177+
}
178+
179+
let parallel_elapsed = parallel_start.elapsed();
180+
println!(
181+
" {} queries k={} (parallel {}×{}): {:.2}ms ({:.3}µs/query)",
182+
total_parallel_queries,
183+
k,
184+
num_threads,
185+
num_queries,
186+
parallel_elapsed.as_secs_f64() * 1000.0,
187+
parallel_elapsed.as_secs_f64() * 1_000_000.0 / total_parallel_queries as f64
188+
);
189+
}
190+
191+
println!("\n{}", "=".repeat(40));
192+
println!("Conclusion:");
193+
println!("The HilbertRTree is safe to share across threads using Arc!");
194+
println!("All queries use &self → lock-free parallel access.");
195+
}
196+
197+
198+
/*
199+
cargo bench --bench profile_parallel
200+
201+
Generating 1000000 random boxes...
202+
Generated in 26.95ms
203+
204+
Building index...
205+
Index built in 133.13ms
206+
207+
Profiling query_intersecting (parallel):
208+
----------------------------------------
209+
10000 small queries (parallel 10×1000): 6.82ms (0.682µs/query)
210+
10000 large queries (parallel 10×1000): 4880.27ms (488.027µs/query)
211+
212+
Profiling query_nearest_k (parallel):
213+
----------------------------------------
214+
10000 queries k=1 (parallel 10×1000): 11.07ms (1.107µs/query)
215+
10000 queries k=10 (parallel 10×1000): 11.86ms (1.186µs/query)
216+
10000 queries k=100 (parallel 10×1000): 24.00ms (2.400µs/query)
217+
1000 queries k=1000 (parallel 10×100): 15.13ms (15.128µs/query)
218+
219+
220+
*/

src/hilbert_rtree.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,8 @@ impl HilbertRTree {
387387
k: usize,
388388
results: &mut Vec<usize>,
389389
) {
390+
results.clear();
390391
if self.num_items == 0 || self.level_bounds.is_empty() || k == 0 {
391-
results.clear();
392392
return;
393393
}
394394

@@ -624,11 +624,10 @@ impl HilbertRTree {
624624
/// // Results contain both box 0 and box 1 (point is inside both)
625625
/// ```
626626
pub fn query_point(&self, x: f64, y: f64, results: &mut Vec<usize>) {
627+
results.clear();
627628
if self.num_items == 0 || self.level_bounds.is_empty() {
628629
return;
629630
}
630-
631-
results.clear();
632631

633632
let mut queue = Vec::new();
634633
let mut node_index = self.total_nodes - 1;
@@ -691,11 +690,10 @@ impl HilbertRTree {
691690
/// // Results contain box 0 and box 1 (both contain the query rectangle)
692691
/// ```
693692
pub fn query_contain(&self, min_x: f64, min_y: f64, max_x: f64, max_y: f64, results: &mut Vec<usize>) {
693+
results.clear();
694694
if self.num_items == 0 || self.level_bounds.is_empty() {
695695
return;
696696
}
697-
698-
results.clear();
699697

700698
let mut queue = Vec::new();
701699
let mut node_index = self.total_nodes - 1;
@@ -757,11 +755,10 @@ impl HilbertRTree {
757755
/// // Results contain only box 1 (box 0 is too large, box 2 is outside)
758756
/// ```
759757
pub fn query_contained_within(&self, min_x: f64, min_y: f64, max_x: f64, max_y: f64, results: &mut Vec<usize>) {
758+
results.clear();
760759
if self.num_items == 0 || self.level_bounds.is_empty() {
761760
return;
762761
}
763-
764-
results.clear();
765762

766763
let mut queue = Vec::new();
767764
let mut node_index = self.total_nodes - 1;
@@ -996,12 +993,10 @@ impl HilbertRTree {
996993
distance: f64,
997994
results: &mut Vec<usize>,
998995
) {
996+
results.clear();
999997
if self.num_items == 0 || self.level_bounds.is_empty() || distance < 0.0 {
1000-
results.clear();
1001998
return;
1002999
}
1003-
1004-
results.clear();
10051000

10061001
// Normalize direction vector
10071002
let dir_len_sq = dir_x * dir_x + dir_y * dir_y;

src/hilbert_rtree_i32.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,10 @@ impl HilbertRTreeI32 {
550550
/// // Results contain both box 0 and box 1 (point is inside both)
551551
/// ```
552552
pub fn query_point(&self, x: i32, y: i32, results: &mut Vec<usize>) {
553+
results.clear();
553554
if self.num_items == 0 || self.level_bounds.is_empty() {
554555
return;
555556
}
556-
557-
results.clear();
558557

559558
let mut queue = Vec::new();
560559
let mut node_index = self.total_nodes - 1;
@@ -617,11 +616,10 @@ impl HilbertRTreeI32 {
617616
/// // Results contain box 0 and box 1 (both contain the query rectangle)
618617
/// ```
619618
pub fn query_contain(&self, min_x: i32, min_y: i32, max_x: i32, max_y: i32, results: &mut Vec<usize>) {
619+
results.clear();
620620
if self.num_items == 0 || self.level_bounds.is_empty() {
621621
return;
622622
}
623-
624-
results.clear();
625623

626624
let mut queue = Vec::new();
627625
let mut node_index = self.total_nodes - 1;
@@ -683,11 +681,10 @@ impl HilbertRTreeI32 {
683681
/// // Results contain only box 1 (box 0 is too large, box 2 is outside)
684682
/// ```
685683
pub fn query_contained_within(&self, min_x: i32, min_y: i32, max_x: i32, max_y: i32, results: &mut Vec<usize>) {
684+
results.clear();
686685
if self.num_items == 0 || self.level_bounds.is_empty() {
687686
return;
688687
}
689-
690-
results.clear();
691688

692689
let mut queue = Vec::new();
693690
let mut node_index = self.total_nodes - 1;

0 commit comments

Comments
 (0)