Skip to content

Commit 494271c

Browse files
Merge pull request #48 from HdrHistogram/percentile.opt
`ValueAtPercentile()` 4.5X on-cpu time optimization: remove expensive condition checks and re-use computation on hotpaths
2 parents 7a2c58a + 8dc0092 commit 494271c

File tree

2 files changed

+42
-20
lines changed

2 files changed

+42
-20
lines changed

hdr.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -331,21 +331,37 @@ func (h *Histogram) ValueAtPercentile(percentile float64) int64 {
331331
percentile = 100
332332
}
333333

334-
total := int64(0)
335334
countAtPercentile := int64(((percentile / 100) * float64(h.totalCount)) + 0.5)
335+
valueFromIdx := h.getValueFromIdxUpToCount(countAtPercentile)
336+
if percentile == 0.0 {
337+
return h.lowestEquivalentValue(valueFromIdx)
338+
}
339+
return h.highestEquivalentValue(valueFromIdx)
340+
}
336341

337-
i := h.iterator()
338-
for i.nextCountAtIdx() {
339-
total += i.countAtIdx
340-
if total >= countAtPercentile {
341-
if percentile == 0.0 {
342-
return h.lowestEquivalentValue(i.valueFromIdx)
343-
}
344-
return h.highestEquivalentValue(i.valueFromIdx)
342+
func (h *Histogram) getValueFromIdxUpToCount(countAtPercentile int64) int64 {
343+
var countToIdx int64
344+
var valueFromIdx int64
345+
var subBucketIdx int32 = -1
346+
var bucketIdx int32
347+
bucketBaseIdx := h.getBucketBaseIdx(bucketIdx)
348+
349+
for {
350+
if countToIdx >= countAtPercentile {
351+
break
352+
}
353+
// increment bucket
354+
subBucketIdx++
355+
if subBucketIdx >= h.subBucketCount {
356+
subBucketIdx = h.subBucketHalfCount
357+
bucketIdx++
358+
bucketBaseIdx = h.getBucketBaseIdx(bucketIdx)
345359
}
346-
}
347360

348-
return 0
361+
countToIdx += h.getCountAtIndexGivenBucketBaseIdx(bucketBaseIdx, subBucketIdx)
362+
valueFromIdx = int64(subBucketIdx) << uint(int64(bucketIdx)+h.unitMagnitude)
363+
}
364+
return valueFromIdx
349365
}
350366

351367
// ValueAtPercentiles, given an slice of percentiles returns a map containing for each passed percentile,
@@ -372,7 +388,7 @@ func (h *Histogram) ValueAtPercentiles(percentiles []float64) (values map[float6
372388
total := int64(0)
373389
currentQuantileSlicePos := 0
374390
i := h.iterator()
375-
for currentQuantileSlicePos < totalQuantilesToCalculate && i.nextCountAtIdx() {
391+
for currentQuantileSlicePos < totalQuantilesToCalculate && i.nextCountAtIdx(h.totalCount) {
376392
total += i.countAtIdx
377393
for currentQuantileSlicePos < totalQuantilesToCalculate && total >= countAtPercentiles[currentQuantileSlicePos] {
378394
currentPercentile := percentiles[currentQuantileSlicePos]
@@ -579,10 +595,16 @@ func (h *Histogram) getCountAtIndex(bucketIdx, subBucketIdx int32) int64 {
579595
return h.counts[h.countsIndex(bucketIdx, subBucketIdx)]
580596
}
581597

598+
func (h *Histogram) getCountAtIndexGivenBucketBaseIdx(bucketBaseIdx, subBucketIdx int32) int64 {
599+
return h.counts[bucketBaseIdx+subBucketIdx-h.subBucketHalfCount]
600+
}
601+
582602
func (h *Histogram) countsIndex(bucketIdx, subBucketIdx int32) int32 {
583-
bucketBaseIdx := (bucketIdx + 1) << uint(h.subBucketHalfCountMagnitude)
584-
offsetInBucket := subBucketIdx - h.subBucketHalfCount
585-
return bucketBaseIdx + offsetInBucket
603+
return h.getBucketBaseIdx(bucketIdx) + subBucketIdx - h.subBucketHalfCount
604+
}
605+
606+
func (h *Histogram) getBucketBaseIdx(bucketIdx int32) int32 {
607+
return (bucketIdx + 1) << uint(h.subBucketHalfCountMagnitude)
586608
}
587609

588610
// return the lowest (and therefore highest precision) bucket index that can represent the value
@@ -622,8 +644,8 @@ type iterator struct {
622644
}
623645

624646
// nextCountAtIdx does not update the iterator highestEquivalentValue in order to optimize cpu usage.
625-
func (i *iterator) nextCountAtIdx() bool {
626-
if i.countToIdx >= i.h.totalCount {
647+
func (i *iterator) nextCountAtIdx(limit int64) bool {
648+
if i.countToIdx >= limit {
627649
return false
628650
}
629651
// increment bucket
@@ -645,7 +667,7 @@ func (i *iterator) nextCountAtIdx() bool {
645667

646668
// Returns the next element in the iteration.
647669
func (i *iterator) next() bool {
648-
if !i.nextCountAtIdx() {
670+
if !i.nextCountAtIdx(i.h.totalCount) {
649671
return false
650672
}
651673
i.highestEquivalentValue = i.h.highestEquivalentValue(i.valueFromIdx)

hdr_benchmark_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ func BenchmarkHistogramValueAtPercentile(b *testing.B) {
4040
var sigfigs = 3
4141
var totalDatapoints = 1000000
4242
h, data := populateHistogramLogNormalDist(b, lowestDiscernibleValue, highestTrackableValue, sigfigs, totalDatapoints)
43-
quantiles := make([]float64, b.N)
43+
quantiles := make([]float64, totalDatapoints)
4444
for i := range quantiles {
4545
data[i] = rand.Float64() * 100.0
4646
}
4747
b.ResetTimer()
4848
b.ReportAllocs()
4949
for i := 0; i < b.N; i++ {
50-
h.ValueAtPercentile(data[i])
50+
h.ValueAtPercentile(data[i%totalDatapoints])
5151
}
5252
}
5353

0 commit comments

Comments
 (0)