@@ -139,12 +139,12 @@ impl<'a, C: GeoConfig<Diff>> GeoDiffCount<'a, C> {
139139 /// [`Self::from_bit_chunks`], which re-establishes the filter invariants. The resulting filter
140140 /// is identical to the one produced by pushing the same hashes one by one.
141141 pub fn from_hashes ( config : C , hashes : impl ExactSizeIterator < Item = u64 > ) -> Self {
142- let split = estimate_split_bucket ( & config, hashes. len ( ) ) ;
142+ let ( split, capacity ) = estimate_split_bucket ( & config, hashes. len ( ) ) ;
143143
144144 // Buckets >= split are sparse and collected for sorting; buckets < split are dense and
145145 // folded directly into the bit vector, where repeated buckets cancel via xor. The toggler
146146 // resolves the bit vector's owned storage once, keeping the per-bit work out of the loop.
147- let mut numbers = Vec :: new ( ) ;
147+ let mut numbers = Vec :: with_capacity ( capacity ) ;
148148 let mut bits = BitVec :: default ( ) ;
149149 bits. resize ( split) ;
150150 {
@@ -479,23 +479,31 @@ pub(crate) fn xor<C: GeoConfig<Diff>>(
479479}
480480
481481/// Estimates the bucket id that separates the sparse most-significant buckets ("numbers") from
482- /// the dense least-significant buckets ("bits") for a filter built from `n` hashes.
482+ /// the dense least-significant buckets ("bits") for a filter built from `n` hashes, and a capacity
483+ /// to preallocate the `numbers` buffer with (the expected count at or above the split, plus
484+ /// headroom for variance).
483485///
484- /// The expected number of hashes falling into buckets `>= s` is `n * phi^s`. We pick `s` such
485- /// that this is a small multiple of the most-significant bucket budget, so that after parity
486- /// reduction the most-significant buckets are almost always supplied by the collected numbers.
487- /// Choosing a slightly lower split only makes the collected set a bit larger; correctness does
488- /// not depend on the estimate, since [`GeoDiffCount::from_bit_chunks`] re-splits the combined
489- /// stream regardless.
490- fn estimate_split_bucket < C : GeoConfig < Diff > > ( config : & C , n : usize ) -> usize {
491- let target = 2 * config. max_msb_len ( ) ;
486+ /// The expected number of hashes falling into buckets `>= s` is `n * phi^s`. We target about
487+ /// `max_msb_len / 2` such hashes: the most-significant buckets do *not* need to be fully supplied
488+ /// by the collected numbers, since [`GeoDiffCount::from_bit_chunks`] re-splits the combined stream
489+ /// and pulls the remainder from the dense bits. Because the buckets are geometric, raising the
490+ /// split by one `bits_per_level` roughly halves the collected set, so a small target keeps the
491+ /// sort cheap while only marginally enlarging the bit vector. Correctness does not depend on the
492+ /// estimate.
493+ fn estimate_split_bucket < C : GeoConfig < Diff > > ( config : & C , n : usize ) -> ( usize , usize ) {
494+ let target = config. max_msb_len ( ) / 2 ;
495+ // The count at or above the split is ~`target` but varies (std ~sqrt(target)), so reserve
496+ // twice as much to avoid reallocating `numbers` -- but never more than the total hash count.
497+ let capacity = ( 2 * target) . min ( n) ;
492498 if n <= target {
493- return 0 ;
499+ // Every hash ends up in `numbers` (split == 0).
500+ return ( 0 , capacity) ;
494501 }
495502 let ratio = target as f64 / n as f64 ;
496- let split = ( ratio. ln ( ) / config. phi_f64 ( ) . ln ( ) ) . floor ( ) as usize ;
497- // No bucket can ever exceed this bound, so never allocate a larger bit vector.
498- split. min ( 64 * config. bits_per_level ( ) )
503+ let split = ( ( ratio. ln ( ) / config. phi_f64 ( ) . ln ( ) ) . floor ( ) as usize )
504+ // No bucket can ever exceed this bound, so never allocate a larger bit vector.
505+ . min ( 64 * config. bits_per_level ( ) ) ;
506+ ( split, capacity)
499507}
500508
501509/// Given a slice sorted in descending order, keeps a single copy of every value occurring an odd
0 commit comments