From 6f55829ba29f044deec8538156f8ce28e3ba3107 Mon Sep 17 00:00:00 2001 From: Seth Stadick Date: Wed, 2 Jul 2025 05:24:15 -0400 Subject: [PATCH] fix: improve perf of count with smarter handling of half-open range The BITS paper uses fully inclusive ranges. When this was written I'm not sure I understood that. To get things to match the naive version of count (find -> count) I had a while loop in the bits count method to advance the cursor past the matched start/stop index. This change was found while porting mojo-lapper and removes the while loop. It also has a more effecient branchless binary search. Benchmarks for count improve 24-30% (see PR). --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/lib.rs | 46 +++++++++++++++------------------------------- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11fb6f1..9c322df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,7 +448,7 @@ checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "rust-lapper" -version = "1.1.0" +version = "1.2.0" dependencies = [ "bincode", "cpu-time", diff --git a/Cargo.toml b/Cargo.toml index e7cfd52..37c3651 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-lapper" -version = "1.1.0" +version = "1.2.0" authors = ["Seth Stadick "] edition = "2018" license = "MIT" diff --git a/src/lib.rs b/src/lib.rs index 549b117..b76a136 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,10 @@ //! - Extremely fast on most genomic datasets. (3-4x faster than other methods) //! - Extremely fast on in order queries. (10x faster than other methods) //! - Extremely fast intersections count method based on the -//! [BITS](https://arxiv.org/pdf/1208.3407.pdf) algorithm +//! [BITS](https://arxiv.org/pdf/1208.3407.pdf) algorithm //! - Parallel friendly. Queries are on an immutable structure, even for seek //! - Consumer / Adapter paradigm, Iterators are returned and serve as the main API for interacting -//! with the lapper +//! with the lapper //! //! ## Details: //! @@ -350,7 +350,7 @@ where if let Some(first) = ivs.next() { stack.push_back(first); for interval in ivs { - let mut top = stack.pop_back().unwrap(); + let top = stack.pop_back().unwrap(); if top.stop < interval.start { stack.push_back(top); stack.push_back(interval); @@ -422,24 +422,20 @@ where where K: PartialEq + PartialOrd, { - if elems.is_empty() { + if elems.is_empty() || elems[0] >= *key { return 0; + } else if elems[elems.len() - 1] < *key { + return elems.len(); } - if elems[0] > *key { - return 0; - } - let mut high = elems.len(); - let mut low = 0; - while high - low > 1 { - let mid = (high + low) / 2; - if elems[mid] < *key { - low = mid; - } else { - high = mid; - } + let mut cursor = 0; + let mut length = elems.len(); + while length > 1 { + let half = length >> 1; + length -= half; + cursor += (usize::from(elems[cursor + half - 1] < *key)) * half; } - high + cursor } /// Find the union and the intersect of two lapper objects. @@ -587,23 +583,11 @@ where #[inline] pub fn count(&self, start: I, stop: I) -> usize { let len = self.intervals.len(); - let mut first = Self::bsearch_seq(start, &self.stops); + // Plus one to account for half-openness of lapper intervals compared to BITS paper + let first = Self::bsearch_seq(start + one::(), &self.stops); let last = Self::bsearch_seq(stop, &self.starts); - //println!("{}/{}", start, stop); - //println!("pre start found in stops: {}: {}", first, self.stops[first]); - //println!("pre stop found in starts: {}", last); - //while last < len && self.starts[last] == stop { - //last += 1; - //} - while first < len && self.stops[first] == start { - first += 1; - } let num_cant_after = len - last; len - first - num_cant_after - //println!("{:#?}", self.starts); - //println!("{:#?}", self.stops); - //println!("start found in stops: {}", first); - //println!("stop found in starts: {}", last); } /// Find all intervals that overlap start .. stop