@@ -507,7 +507,8 @@ impl JamReader {
507507 }
508508
509509 let entries = self . bucket_entries ( bucket_idx) ;
510- self . interpolation_search ( entries, hash) . is_some ( )
510+ let idx = lower_bound_hash ( entries, hash) ;
511+ entries. get ( idx) . is_some_and ( |entry| entry. hash == hash)
511512 }
512513
513514 #[ inline]
@@ -524,65 +525,77 @@ impl JamReader {
524525 & [ ]
525526 } ;
526527
527- let start = if entries. is_empty ( ) {
528- 0
529- } else {
530- self . interpolation_find_start ( entries, hash)
531- } ;
528+ let start = lower_bound_hash ( entries, hash) ;
532529
533530 entries[ start..]
534531 . iter ( )
535- . skip_while ( move |e| e. hash < hash)
536532 . take_while ( move |e| e. hash == hash)
537533 . map ( |e| e. sample_id )
538534 }
535+ }
539536
540- fn interpolation_search ( & self , entries : & [ Entry ] , key : u64 ) -> Option < usize > {
541- if entries . is_empty ( ) {
542- return None ;
543- }
537+ # [ inline ]
538+ pub ( crate ) fn lower_bound_hash ( entries : & [ Entry ] , key : u64 ) -> usize {
539+ const MAX_INTERPOLATION_PROBES : usize = 8 ;
540+ const BINARY_SEARCH_CUTOFF : usize = 64 ;
544541
545- let start = self . interpolation_find_start ( entries, key) ;
542+ let mut lo = 0usize ;
543+ let mut hi = entries. len ( ) ;
544+ if hi == 0 {
545+ return 0 ;
546+ }
546547
547- for ( i, entry) in entries[ start..] . iter ( ) . enumerate ( ) {
548- if entry. hash == key {
549- return Some ( start + i) ;
550- }
551- if entry. hash > key {
552- break ;
553- }
548+ if entries[ lo] . hash >= key {
549+ return lo;
550+ }
551+ if entries[ hi - 1 ] . hash < key {
552+ return hi;
553+ }
554+
555+ for _ in 0 ..MAX_INTERPOLATION_PROBES {
556+ if hi - lo <= BINARY_SEARCH_CUTOFF {
557+ break ;
554558 }
555559
556- None
557- }
560+ let lo_hash = entries[ lo] . hash ;
561+ let hi_hash = entries[ hi - 1 ] . hash ;
562+ if lo_hash >= key {
563+ return lo;
564+ }
565+ if hi_hash < key {
566+ return hi;
567+ }
568+ if lo_hash == hi_hash {
569+ break ;
570+ }
558571
559- #[ inline]
560- fn interpolation_find_start ( & self , entries : & [ Entry ] , key : u64 ) -> usize {
561- let count = entries. len ( ) ;
562- let threshold = self . threshold ( ) ;
572+ let width = hi - lo - 1 ;
573+ let span = hi_hash - lo_hash;
574+ let rel = key. saturating_sub ( lo_hash) ;
575+ let probe = lo + ( ( rel as u128 * width as u128 ) / span as u128 ) as usize ;
576+ let probe_hash = entries[ probe] . hash ;
563577
564- let est = ( ( key as u128 * count as u128 ) / threshold as u128 ) as usize ;
578+ if probe_hash < key {
579+ lo = probe + 1 ;
580+ } else {
581+ hi = probe + 1 ;
582+ }
583+ }
565584
566- let est = est. saturating_sub ( 16 ) . min ( count - 1 ) ;
585+ lower_bound_hash_binary ( entries, key, lo, hi)
586+ }
567587
568- if entries [ est ] . hash > key {
569- let mut i = est ;
570- while i > 0 && entries [ i - 1 ] . hash >= key {
571- i -= 1 ;
572- }
573- i
588+ # [ inline ]
589+ fn lower_bound_hash_binary ( entries : & [ Entry ] , key : u64 , mut lo : usize , mut hi : usize ) -> usize {
590+ while lo < hi {
591+ let mid = lo + ( hi - lo ) / 2 ;
592+ if entries [ mid ] . hash < key {
593+ lo = mid + 1 ;
574594 } else {
575- if entries[ est] . hash == key {
576- let mut i = est;
577- while i > 0 && entries[ i - 1 ] . hash == key {
578- i -= 1 ;
579- }
580- i
581- } else {
582- est
583- }
595+ hi = mid;
584596 }
585597 }
598+ lo
586599}
587600
588601fn checked_file_range (
@@ -854,6 +867,26 @@ mod tests {
854867 assert_eq ! ( samples, ( 0 ..entry_count as u32 ) . collect:: <Vec <_>>( ) ) ;
855868 }
856869
870+ #[ test]
871+ fn test_lower_bound_hash_handles_skewed_distribution ( ) {
872+ let mut hashes: Vec < u64 > = ( 0 ..9000 ) . map ( |i| i * 2 ) . collect ( ) ;
873+ hashes. extend ( ( 0 ..1000 ) . map ( |i| 1_000_000 + i * 2 ) ) ;
874+ hashes. splice ( 4500 ..4500 , [ 9000 , 9000 , 9000 ] ) ;
875+
876+ let entries: Vec < Entry > = hashes
877+ . iter ( )
878+ . enumerate ( )
879+ . map ( |( sample_id, & hash) | Entry :: new ( hash, sample_id as u32 ) )
880+ . collect ( ) ;
881+
882+ for key in [
883+ 0 , 1 , 8999 , 9000 , 9001 , 20_000 , 1_000_000 , 1_001_999 , 2_000_000 ,
884+ ] {
885+ let expected = entries. partition_point ( |entry| entry. hash < key) ;
886+ assert_eq ! ( lower_bound_hash( & entries, key) , expected, "key {key}" ) ;
887+ }
888+ }
889+
857890 #[ test]
858891 fn test_reader_open_rejects_malformed_bucket_entry_range ( ) {
859892 let mut bucket_table = vec ! [ BucketMeta :: default ( ) ; BUCKET_COUNT ] ;
0 commit comments