Skip to content

Commit 2b11a4b

Browse files
authored
Merge pull request #1673 from imorph/faster_find_bucket
PERF: faster algorithm to discover bucket of an histogram observation
2 parents fcfad5c + 78d7a94 commit 2b11a4b

File tree

2 files changed

+117
-9
lines changed

2 files changed

+117
-9
lines changed

prometheus/histogram.go

+29-9
Original file line numberDiff line numberDiff line change
@@ -858,15 +858,35 @@ func (h *histogram) Write(out *dto.Metric) error {
858858
// findBucket returns the index of the bucket for the provided value, or
859859
// len(h.upperBounds) for the +Inf bucket.
860860
func (h *histogram) findBucket(v float64) int {
861-
// TODO(beorn7): For small numbers of buckets (<30), a linear search is
862-
// slightly faster than the binary search. If we really care, we could
863-
// switch from one search strategy to the other depending on the number
864-
// of buckets.
865-
//
866-
// Microbenchmarks (BenchmarkHistogramNoLabels):
867-
// 11 buckets: 38.3 ns/op linear - binary 48.7 ns/op
868-
// 100 buckets: 78.1 ns/op linear - binary 54.9 ns/op
869-
// 300 buckets: 154 ns/op linear - binary 61.6 ns/op
861+
n := len(h.upperBounds)
862+
if n == 0 {
863+
return 0
864+
}
865+
866+
// Early exit: if v is less than or equal to the first upper bound, return 0
867+
if v <= h.upperBounds[0] {
868+
return 0
869+
}
870+
871+
// Early exit: if v is greater than the last upper bound, return len(h.upperBounds)
872+
if v > h.upperBounds[n-1] {
873+
return n
874+
}
875+
876+
// For small arrays, use simple linear search
877+
// "magic number" 35 is result of tests on couple different (AWS and baremetal) servers
878+
// see more details here: https://github.com/prometheus/client_golang/pull/1662
879+
if n < 35 {
880+
for i, bound := range h.upperBounds {
881+
if v <= bound {
882+
return i
883+
}
884+
}
885+
// If v is greater than all upper bounds, return len(h.upperBounds)
886+
return n
887+
}
888+
889+
// For larger arrays, use stdlib's binary search
870890
return sort.SearchFloat64s(h.upperBounds, v)
871891
}
872892

prometheus/histogram_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -1455,3 +1455,91 @@ func compareNativeExemplarValues(t *testing.T, exps []*dto.Exemplar, values []fl
14551455
}
14561456
}
14571457
}
1458+
1459+
var resultFindBucket int
1460+
1461+
func benchmarkFindBucket(b *testing.B, l int) {
1462+
h := &histogram{upperBounds: make([]float64, l)}
1463+
for i := range h.upperBounds {
1464+
h.upperBounds[i] = float64(i)
1465+
}
1466+
v := float64(l / 2)
1467+
1468+
b.ResetTimer()
1469+
for i := 0; i < b.N; i++ {
1470+
resultFindBucket = h.findBucket(v)
1471+
}
1472+
}
1473+
1474+
func BenchmarkFindBucketShort(b *testing.B) {
1475+
benchmarkFindBucket(b, 20)
1476+
}
1477+
1478+
func BenchmarkFindBucketMid(b *testing.B) {
1479+
benchmarkFindBucket(b, 40)
1480+
}
1481+
1482+
func BenchmarkFindBucketLarge(b *testing.B) {
1483+
benchmarkFindBucket(b, 100)
1484+
}
1485+
1486+
func BenchmarkFindBucketHuge(b *testing.B) {
1487+
benchmarkFindBucket(b, 500)
1488+
}
1489+
1490+
func BenchmarkFindBucketInf(b *testing.B) {
1491+
h := &histogram{upperBounds: make([]float64, 500)}
1492+
for i := range h.upperBounds {
1493+
h.upperBounds[i] = float64(i)
1494+
}
1495+
v := 1000.5
1496+
1497+
b.ResetTimer()
1498+
for i := 0; i < b.N; i++ {
1499+
resultFindBucket = h.findBucket(v)
1500+
}
1501+
}
1502+
1503+
func BenchmarkFindBucketLow(b *testing.B) {
1504+
h := &histogram{upperBounds: make([]float64, 500)}
1505+
for i := range h.upperBounds {
1506+
h.upperBounds[i] = float64(i)
1507+
}
1508+
v := -1.1
1509+
1510+
b.ResetTimer()
1511+
for i := 0; i < b.N; i++ {
1512+
resultFindBucket = h.findBucket(v)
1513+
}
1514+
}
1515+
1516+
func TestFindBucket(t *testing.T) {
1517+
smallHistogram := &histogram{upperBounds: []float64{1, 2, 3, 4, 5}}
1518+
largeHistogram := &histogram{upperBounds: make([]float64, 50)}
1519+
for i := range largeHistogram.upperBounds {
1520+
largeHistogram.upperBounds[i] = float64(i)
1521+
}
1522+
1523+
tests := []struct {
1524+
h *histogram
1525+
v float64
1526+
expected int
1527+
}{
1528+
{smallHistogram, -1, 0},
1529+
{smallHistogram, 0.5, 0},
1530+
{smallHistogram, 2.5, 2},
1531+
{smallHistogram, 5.5, 5},
1532+
{largeHistogram, -1, 0},
1533+
{largeHistogram, 25.5, 26},
1534+
{largeHistogram, 49.5, 50},
1535+
{largeHistogram, 50.5, 50},
1536+
{largeHistogram, 5000.5, 50},
1537+
}
1538+
1539+
for _, tt := range tests {
1540+
result := tt.h.findBucket(tt.v)
1541+
if result != tt.expected {
1542+
t.Errorf("findBucket(%v) = %d; expected %d", tt.v, result, tt.expected)
1543+
}
1544+
}
1545+
}

0 commit comments

Comments
 (0)