Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

tangled-core / bobbin / crates / edge-index / tests / bucket_scaling.rs
7.1 kB 231 lines
1use std::cell::Cell; 2use std::cmp::Ordering; 3use std::collections::BTreeSet; 4use std::ops::Bound; 5 6type Key = (u64, u32); 7 8fn splitmix64(state: &mut u64) -> u64 { 9 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); 10 let mut z = *state; 11 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); 12 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); 13 z ^ (z >> 31) 14} 15 16fn adversarial_keys(n: u32) -> Vec<Key> { 17 (0..n) 18 .scan(0xD1CE_5EED_u64, |state, i| Some((splitmix64(state), i))) 19 .collect() 20} 21 22fn vec_shift_work(keys: &[Key]) -> u128 { 23 let mut sorted: Vec<Key> = Vec::with_capacity(keys.len()); 24 keys.iter().fold(0u128, |moves, &key| { 25 match sorted.binary_search(&key) { 26 Ok(_) => moves, 27 Err(pos) => { 28 let displaced = (sorted.len() - pos) as u128; 29 sorted.insert(pos, key); 30 moves + displaced 31 } 32 } 33 }) 34} 35 36thread_local! { 37 static COMPARES: Cell<u128> = Cell::new(0); 38} 39 40#[derive(PartialEq, Eq)] 41struct CountedKey(Key); 42 43impl PartialOrd for CountedKey { 44 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { 45 Some(self.cmp(other)) 46 } 47} 48 49impl Ord for CountedKey { 50 fn cmp(&self, other: &Self) -> Ordering { 51 COMPARES.with(|c| c.set(c.get() + 1)); 52 self.0.cmp(&other.0) 53 } 54} 55 56fn btree_compare_work(keys: &[Key]) -> u128 { 57 COMPARES.with(|c| c.set(0)); 58 let mut tree: BTreeSet<CountedKey> = BTreeSet::new(); 59 keys.iter().for_each(|&key| { 60 tree.insert(CountedKey(key)); 61 }); 62 COMPARES.with(Cell::get) 63} 64 65fn doubling_ratios(work: &[u128]) -> Vec<f64> { 66 work.windows(2) 67 .map(|w| w[1] as f64 / w[0] as f64) 68 .collect() 69} 70 71#[test] 72fn sorted_vec_build_is_quadratic_while_btree_is_quasilinear() { 73 let sizes = [2_048u32, 4_096, 8_192, 16_384]; 74 let measured: Vec<(u128, u128)> = sizes 75 .iter() 76 .map(|&n| { 77 let keys = adversarial_keys(n); 78 (vec_shift_work(&keys), btree_compare_work(&keys)) 79 }) 80 .collect(); 81 82 let vec_work: Vec<u128> = measured.iter().map(|m| m.0).collect(); 83 let btree_work: Vec<u128> = measured.iter().map(|m| m.1).collect(); 84 85 eprintln!("{:<8} {:>16} {:>16}", "N", "vec_shifts", "btree_compares"); 86 sizes.iter().enumerate().for_each(|(i, n)| { 87 eprintln!("{:<8} {:>16} {:>16}", n, vec_work[i], btree_work[i]); 88 }); 89 90 let vec_ratios = doubling_ratios(&vec_work); 91 let btree_ratios = doubling_ratios(&btree_work); 92 eprintln!("vec doubling ratios: {vec_ratios:?}"); 93 eprintln!("btree doubling ratios: {btree_ratios:?}"); 94 95 vec_ratios.iter().for_each(|&ratio| { 96 assert!( 97 (3.5..=4.5).contains(&ratio), 98 "sorted-vec build must quadruple per doubling of N, proving O(n^2), got {ratio:.2}x" 99 ); 100 }); 101 102 btree_ratios.iter().for_each(|&ratio| { 103 assert!( 104 ratio < 3.0, 105 "btree build must stay near a doubling per doubling of N, proving sub-quadratic, got {ratio:.2}x" 106 ); 107 }); 108 109 let advantage: Vec<f64> = (0..sizes.len()) 110 .map(|i| vec_work[i] as f64 / btree_work[i] as f64) 111 .collect(); 112 advantage.windows(2).for_each(|w| { 113 assert!( 114 w[1] > w[0], 115 "btree's advantage over the sorted vec must widen as N grows, got {advantage:?}" 116 ); 117 }); 118} 119 120fn vec_remove_work(keys: &[Key]) -> u128 { 121 let mut sorted: Vec<Key> = keys.to_vec(); 122 sorted.sort_unstable(); 123 keys.iter().fold(0u128, |moves, key| { 124 match sorted.binary_search(key) { 125 Ok(pos) => { 126 let displaced = (sorted.len() - pos - 1) as u128; 127 sorted.remove(pos); 128 moves + displaced 129 } 130 Err(_) => moves, 131 } 132 }) 133} 134 135fn btree_remove_work(keys: &[Key]) -> u128 { 136 let mut tree: BTreeSet<CountedKey> = keys.iter().map(|&k| CountedKey(k)).collect(); 137 COMPARES.with(|c| c.set(0)); 138 keys.iter().for_each(|&k| { 139 tree.remove(&CountedKey(k)); 140 }); 141 COMPARES.with(Cell::get) 142} 143 144fn sample_cursors(keys: &[Key]) -> Vec<Key> { 145 let mut sorted = keys.to_vec(); 146 sorted.sort_unstable(); 147 let step = (sorted.len() / 256).max(1); 148 sorted.iter().step_by(step).copied().collect() 149} 150 151fn vec_seek_work(keys: &[Key], cursors: &[Key]) -> u128 { 152 let mut sorted: Vec<CountedKey> = keys.iter().map(|&k| CountedKey(k)).collect(); 153 sorted.sort_unstable(); 154 COMPARES.with(|c| c.set(0)); 155 cursors.iter().for_each(|&cur| { 156 let _ = sorted.partition_point(|k| *k <= CountedKey(cur)); 157 }); 158 COMPARES.with(Cell::get) 159} 160 161fn btree_seek_work(keys: &[Key], cursors: &[Key]) -> u128 { 162 let tree: BTreeSet<CountedKey> = keys.iter().map(|&k| CountedKey(k)).collect(); 163 COMPARES.with(|c| c.set(0)); 164 cursors.iter().for_each(|&cur| { 165 let _ = tree 166 .range((Bound::Excluded(CountedKey(cur)), Bound::Unbounded)) 167 .next(); 168 }); 169 COMPARES.with(Cell::get) 170} 171 172#[test] 173fn sorted_vec_teardown_is_quadratic_while_btree_is_quasilinear() { 174 let sizes = [2_048u32, 4_096, 8_192, 16_384]; 175 let measured: Vec<(u128, u128)> = sizes 176 .iter() 177 .map(|&n| { 178 let keys = adversarial_keys(n); 179 (vec_remove_work(&keys), btree_remove_work(&keys)) 180 }) 181 .collect(); 182 183 let vec_ratios = doubling_ratios(&measured.iter().map(|m| m.0).collect::<Vec<_>>()); 184 let btree_ratios = doubling_ratios(&measured.iter().map(|m| m.1).collect::<Vec<_>>()); 185 eprintln!("teardown vec doubling ratios: {vec_ratios:?}"); 186 eprintln!("teardown btree doubling ratios: {btree_ratios:?}"); 187 188 vec_ratios.iter().for_each(|&ratio| { 189 assert!( 190 (3.5..=4.5).contains(&ratio), 191 "sorted-vec teardown must quadruple per doubling, proving O(n^2), got {ratio:.2}x" 192 ); 193 }); 194 btree_ratios.iter().for_each(|&ratio| { 195 assert!( 196 ratio < 3.0, 197 "btree teardown must stay sub-quadratic, got {ratio:.2}x" 198 ); 199 }); 200} 201 202#[test] 203fn cursor_seek_is_sublinear_for_both_representations() { 204 let sizes = [2_048u32, 4_096, 8_192, 16_384]; 205 let measured: Vec<(u128, u128)> = sizes 206 .iter() 207 .map(|&n| { 208 let keys = adversarial_keys(n); 209 let cursors = sample_cursors(&keys); 210 ( 211 vec_seek_work(&keys, &cursors), 212 btree_seek_work(&keys, &cursors), 213 ) 214 }) 215 .collect(); 216 217 let vec_ratios = doubling_ratios(&measured.iter().map(|m| m.0).collect::<Vec<_>>()); 218 let btree_ratios = doubling_ratios(&measured.iter().map(|m| m.1).collect::<Vec<_>>()); 219 eprintln!("seek vec doubling ratios: {vec_ratios:?}"); 220 eprintln!("seek btree doubling ratios: {btree_ratios:?}"); 221 222 vec_ratios 223 .iter() 224 .chain(btree_ratios.iter()) 225 .for_each(|&ratio| { 226 assert!( 227 ratio < 1.8, 228 "a fixed cursor sample must seek in logarithmic work as N grows, got {ratio:.2}x" 229 ); 230 }); 231}