Skip to content

Commit e6d0357

Browse files
committed
feat: Update query interpolation to be smarter in bias aware mode
1 parent 2837bfc commit e6d0357

2 files changed

Lines changed: 77 additions & 55 deletions

File tree

src/query.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::bias::HashBiasTable;
22
use crate::core_utils::passes_entropy_filter;
33
use crate::format::{BUCKET_COUNT, bucket_id};
4-
use crate::reader::{JamReader, ReaderError};
4+
use crate::reader::{JamReader, ReaderError, lower_bound_hash};
55
use jamhash::jamhash_u64;
66
use needletail::{Sequence, parse_fastx_file};
77
use std::collections::{HashMap, HashSet};
@@ -577,18 +577,7 @@ impl QueryEngine {
577577
while q_idx < survivors.len() {
578578
let q_hash = survivors[q_idx].0;
579579

580-
let est = ((q_hash as u128 * count as u128) / threshold as u128) as usize;
581-
let mut d_idx = est.saturating_sub(16).min(count.saturating_sub(1));
582-
583-
while d_idx > 0 && db_bucket[d_idx].hash > q_hash {
584-
d_idx -= 1;
585-
}
586-
while d_idx < count && db_bucket[d_idx].hash < q_hash {
587-
d_idx += 1;
588-
}
589-
while d_idx > 0 && db_bucket[d_idx - 1].hash == q_hash {
590-
d_idx -= 1;
591-
}
580+
let d_idx = lower_bound_hash(db_bucket, q_hash);
592581

593582
let current_byte = entry_start + d_idx * ENTRY_SIZE;
594583
let current_page = current_byte & !(PAGE_SIZE - 1);

src/reader.rs

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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

588601
fn 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

Comments
 (0)