This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 5190e33 perf(tdigest): simplify rank anchor lookup (#290)
5190e33 is described below
commit 5190e33b210b587c3d7c8f03ba8288dfcc361565
Author: tison <[email protected]>
AuthorDate: Mon Oct 5 09:10:20 2026 +0800
perf(tdigest): simplify rank anchor lookup (#290)
---
CHANGELOG.md | 2 +-
benchmarks/tdigest/query.rs | 9 ++++++
datasketches/src/tdigest/sketch.rs | 42 ++++++-------------------
tests-integration/tests/tdigest_test/sketch.rs | 43 ++++++++++++++++++++++++++
4 files changed, 62 insertions(+), 34 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d4b1de..4f366e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@ All significant changes to this project will be documented in
this file.
### Improvements
* Serializing buffered `TDigestMut` inputs is faster when their total weight
is at most `k / 2`.
-* `TDigest` and `TDigestMut` `cdf` and `pmf` queries are faster when answering
many split points.
+* `TDigest` and `TDigestMut` `rank` queries are faster for values between
centroid means; `cdf` and `pmf` queries are faster when answering many split
points.
* Improve the readability of `Debug` output for HLL and CPC sketches and
unions.
* `KllSketch::quantile` and `ReqSketch::quantile` are faster at ranks `0.0`
and `1.0`, with query work independent of the number of retained samples.
* `BloomFilter::insert` is faster for integer and raw-byte inputs.
`BloomFilter::contains_and_insert` is also faster when checking already-present
integer values.
diff --git a/benchmarks/tdigest/query.rs b/benchmarks/tdigest/query.rs
index ae36977..338179e 100644
--- a/benchmarks/tdigest/query.rs
+++ b/benchmarks/tdigest/query.rs
@@ -22,6 +22,7 @@ use rand::SeedableRng;
use rand::rngs::StdRng;
use rand::seq::SliceRandom;
+use super::support::build_digest;
use super::support::prepared_digest;
#[divan::bench]
@@ -31,6 +32,14 @@ fn rank(bencher: Bencher) {
bencher.bench_local(|| black_box(&digest).rank(black_box(0.531_25)));
}
+#[divan::bench(args = [0.0, 3.0, 3.5, 7.0])]
+fn rank_repeated_means(bencher: Bencher, value: f64) {
+ let values = (0..100_000).map(|i| (i % 8) as f64).collect::<Vec<_>>();
+ let digest = build_digest(&values);
+
+ bencher.bench_local(|| black_box(&digest).rank(black_box(value)));
+}
+
#[divan::bench]
fn quantile(bencher: Bencher) {
let digest = prepared_digest();
diff --git a/datasketches/src/tdigest/sketch.rs
b/datasketches/src/tdigest/sketch.rs
index 4b6a27d..810c86b 100644
--- a/datasketches/src/tdigest/sketch.rs
+++ b/datasketches/src/tdigest/sketch.rs
@@ -16,7 +16,6 @@
// under the License.
use std::cmp::Ordering;
-use std::convert::identity;
use std::num::NonZeroU64;
use crate::codec::SketchBytes;
@@ -1708,22 +1707,15 @@ impl TDigestView<'_> {
};
}
- let mut lower = self
- .centroids
- .binary_search_by(|c| centroid_lower_bound(c, value))
- .unwrap_or_else(identity);
- assert_ne!(lower, num_centroids, "get_rank: lower == end");
- let mut upper = self
- .centroids
- .binary_search_by(|c| centroid_upper_bound(c, value))
- .unwrap_or_else(identity);
- assert_ne!(upper, 0, "get_rank: upper == begin");
- if value < self.centroids[lower].mean {
- lower -= 1;
- }
- if (upper == num_centroids) || (self.centroids[upper - 1].mean >=
value) {
- upper -= 1;
- }
+ // The tail checks place value within the centroid means. Between
means, the first
+ // mean >= value and its predecessor bracket it; at a mean, use the
entire equal run.
+ let index = self.centroids.partition_point(|c| c.mean < value);
+ let (lower, upper) = if self.centroids[index].mean == value {
+ let end = self.centroids.partition_point(|c| c.mean <= value);
+ (index, end - 1)
+ } else {
+ (index - 1, index)
+ };
let weight_below = weight_before(lower);
let weight_between: u64 = self.centroids[lower..upper]
@@ -2001,22 +1993,6 @@ fn merge_sorted_centroids(left: &mut Vec<Centroid>,
right: &[Centroid]) {
}
}
-fn centroid_lower_bound(c: &Centroid, value: f64) -> Ordering {
- if c.mean < value {
- Ordering::Less
- } else {
- Ordering::Greater
- }
-}
-
-fn centroid_upper_bound(c: &Centroid, value: f64) -> Ordering {
- if c.mean > value {
- Ordering::Greater
- } else {
- Ordering::Less
- }
-}
-
#[derive(Debug, Clone, Copy, PartialEq)]
struct Centroid {
mean: f64,
diff --git a/tests-integration/tests/tdigest_test/sketch.rs
b/tests-integration/tests/tdigest_test/sketch.rs
index 428d96c..b5bb67e 100644
--- a/tests-integration/tests/tdigest_test/sketch.rs
+++ b/tests-integration/tests/tdigest_test/sketch.rs
@@ -244,6 +244,49 @@ fn test_repeated_blocks() {
assert_eq!(tdigest.rank(3.01), Some(1.0));
}
+#[test]
+fn test_rank_brackets_weighted_equal_mean_runs() {
+ let mut digest = deserialize_with_centroids(
+ 100,
+ 0.,
+ 20.,
+ &[
+ (0., 2),
+ (0., 6),
+ (10., 4),
+ (10., 8),
+ (10., 12),
+ (20., 14),
+ (20., 18),
+ ],
+ );
+ // The centroid centers are 1, 5, 10, 16, 26, 39, and 55. At an equal-mean
run,
+ // rank averages the first and last centers; between runs it uses the
adjacent centers.
+ let points = [-1., 0., 5., 10., 15., 20., 21.];
+ let expected = [0., 3., 7.5, 18., 32.5, 47., 64.].map(|mass| mass / 64.);
+ let frozen = digest.clone().freeze();
+ for (&point, &rank) in points.iter().zip(&expected) {
+ assert_eq!(digest.rank(point), Some(rank));
+ assert_eq!(frozen.rank(point), Some(rank));
+ }
+ let mut cdf = expected.to_vec();
+ cdf.push(1.);
+ assert_eq!(digest.cdf(&points), Some(cdf.clone()));
+ assert_eq!(frozen.cdf(&points), Some(cdf));
+ let pmf = vec![
+ 0.,
+ 3. / 64.,
+ 4.5 / 64.,
+ 10.5 / 64.,
+ 14.5 / 64.,
+ 14.5 / 64.,
+ 17. / 64.,
+ 0.,
+ ];
+ assert_eq!(digest.pmf(&points), Some(pmf.clone()));
+ assert_eq!(frozen.pmf(&points), Some(pmf));
+}
+
#[test]
fn test_merge_small() {
let mut td1 = TDigestMut::new(10).unwrap();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]