Skip to content

Commit b308838

Browse files
committed
Improved quantile algorithm to only iterate once over the buckets
1 parent 7c99c81 commit b308838

File tree

2 files changed

+77
-19
lines changed

2 files changed

+77
-19
lines changed

libs/exponential-histogram/src/main/java/org/elasticsearch/exponentialhistogram/ExponentialHistogramQuantile.java

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,88 @@ public static double getQuantile(ExponentialHistogram histo, double quantile) {
4545
long upperRank = (long) Math.ceil(exactRank);
4646
double upperFactor = exactRank - lowerRank;
4747

48-
// TODO: This can be optimized to iterate over the buckets once instead of twice.
49-
return getElementAtRank(histo, lowerRank, negCount, zeroCount) * (1 - upperFactor) + getElementAtRank(
50-
histo,
51-
upperRank,
52-
negCount,
53-
zeroCount
54-
) * upperFactor;
48+
ValueAndPreviousValue values = getElementAtRank(histo, upperRank);
49+
50+
if (lowerRank == upperRank) {
51+
return values.valueAtRank();
52+
} else {
53+
return values.valueAtPreviousRank() * (1 - upperFactor) + values.valueAtRank() * upperFactor;
54+
}
5555
}
5656

57-
private static double getElementAtRank(ExponentialHistogram histo, long rank, long negCount, long zeroCount) {
58-
if (rank < negCount) {
59-
return -getBucketMidpointForRank(histo.negativeBuckets().iterator(), (negCount - 1) - rank);
60-
} else if (rank < (negCount + zeroCount)) {
61-
return 0.0;
57+
/**
58+
* @param valueAtPreviousRank the value at the rank before the desired rank, NaN if not applicable.
59+
* @param valueAtRank the value at the desired rank
60+
*/
61+
private record ValueAndPreviousValue(double valueAtPreviousRank, double valueAtRank
62+
) {
63+
ValueAndPreviousValue negateAndSwap() {
64+
return new ValueAndPreviousValue(-valueAtRank, -valueAtPreviousRank);
65+
}
66+
}
67+
68+
private static ValueAndPreviousValue getElementAtRank(ExponentialHistogram histo, long rank) {
69+
long negativeValuesCount = histo.negativeBuckets().valueCount();
70+
long zeroCount = histo.zeroBucket().count();
71+
if (rank < negativeValuesCount) {
72+
if (negativeValuesCount == 1) {
73+
return new ValueAndPreviousValue(Double.NaN, -getFirstBucketMidpoint(histo.negativeBuckets()));
74+
} else {
75+
return getBucketMidpointForRank(histo.negativeBuckets().iterator(), negativeValuesCount - rank - 1).negateAndSwap();
76+
}
77+
} else if (rank < (negativeValuesCount + zeroCount)) {
78+
if (rank == negativeValuesCount) {
79+
// the element at the previous rank falls into the negative bucket range
80+
return new ValueAndPreviousValue(-getFirstBucketMidpoint(histo.negativeBuckets()), 0.0);
81+
} else {
82+
return new ValueAndPreviousValue(0.0, 0.0);
83+
}
84+
} else {
85+
ValueAndPreviousValue result = getBucketMidpointForRank(histo.positiveBuckets().iterator(), rank - negativeValuesCount - zeroCount);
86+
if ( (rank-1) < negativeValuesCount) {
87+
// previous value falls into the negative bucket range or is -1
88+
return new ValueAndPreviousValue(-getFirstBucketMidpoint(histo.negativeBuckets()), result.valueAtRank);
89+
} else if ( (rank-1) < (negativeValuesCount + zeroCount) ) {
90+
// previous value falls into the zero bucket
91+
return new ValueAndPreviousValue(0.0, result.valueAtRank);
92+
} else {
93+
return result;
94+
}
95+
}
96+
}
97+
98+
private static double getFirstBucketMidpoint(ExponentialHistogram.Buckets buckets) {
99+
CopyableBucketIterator iterator = buckets.iterator();
100+
if (iterator.hasNext()) {
101+
return ExponentialScaleUtils.getPointOfLeastRelativeError(iterator.peekIndex(), iterator.scale());
62102
} else {
63-
return getBucketMidpointForRank(histo.positiveBuckets().iterator(), rank - (negCount + zeroCount));
103+
return Double.NaN;
64104
}
65105
}
66106

67-
private static double getBucketMidpointForRank(BucketIterator buckets, long rank) {
107+
private static ValueAndPreviousValue getBucketMidpointForRank(BucketIterator buckets, long rank) {
108+
long prevIndex = Long.MIN_VALUE;
68109
long seenCount = 0;
69110
while (buckets.hasNext()) {
70111
seenCount += buckets.peekCount();
71112
if (rank < seenCount) {
72-
return ExponentialScaleUtils.getPointOfLeastRelativeError(buckets.peekIndex(), buckets.scale());
113+
double center = ExponentialScaleUtils.getPointOfLeastRelativeError(buckets.peekIndex(), buckets.scale());
114+
double prevCenter;
115+
if (rank > 0) {
116+
if (buckets.peekCount() > 1) {
117+
// element at previous rank is in same bucket
118+
prevCenter = center;
119+
} else {
120+
// element at previous rank is in the previous bucket
121+
prevCenter = ExponentialScaleUtils.getPointOfLeastRelativeError(prevIndex, buckets.scale());
122+
}
123+
} else {
124+
// there is no previous element
125+
prevCenter = Double.NaN;
126+
}
127+
return new ValueAndPreviousValue(prevCenter, center);
73128
}
129+
prevIndex = buckets.peekIndex();
74130
buckets.advance();
75131
}
76132
throw new IllegalStateException("The total number of elements in the buckets is less than the desired rank.");

libs/exponential-histogram/src/test/java/org/elasticsearch/exponentialhistogram/QuantileAccuracyTests.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@ public void testBasicSmall() {
7070
}
7171

7272
public void testPercentileOverlapsZeroBucket() {
73-
ExponentialHistogram histo = ExponentialHistogramGenerator.createFor(-1, 0, 1);
74-
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 0.5), equalTo(0.0));
75-
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 0.375), closeTo(-0.5, 0.000001));
76-
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 0.625), closeTo(0.5, 0.000001));
73+
ExponentialHistogram histo = ExponentialHistogramGenerator.createFor(-2,-1, 0, 0, 0, 1, 1);
74+
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 8.0 / 16.0), equalTo(0.0));
75+
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 7.0 / 16.0), equalTo(0.0));
76+
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 9.0 / 16.0), equalTo(0.0));
77+
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 5.0 / 16.0), closeTo(-0.5, 0.000001));
78+
assertThat(ExponentialHistogramQuantile.getQuantile(histo, 11.0 / 16.0), closeTo(0.5, 0.000001));
7779
}
7880

7981
public void testBigJump() {

0 commit comments

Comments
 (0)