Avoid iterating on maps
Speed up `InsertTargeted*` functions by at least 2x by avoiding
iterating on maps.
Before and after benchmark comparison:
benchmark old ns/op new ns/op delta
BenchmarkInsertTargeted-4 177 73.2 -58.64%
BenchmarkInsertTargetedSmallEpsilon-4 322 94.6 -70.62%
BenchmarkInsertBiased-4 75.8 74.7 -1.45%
BenchmarkInsertBiasedSmallEpsilon-4 563 571 +1.42%
BenchmarkQuery-4 7426 1118 -84.94%
BenchmarkQuerySmallEpsilon-4 80390 12535 -84.41%
benchmark old allocs new allocs delta
BenchmarkInsertTargeted-4 0 0 +0.00%
benchmark old bytes new bytes delta
BenchmarkInsertTargeted-4 0 0 +0.00%
I considered changing the function signature and requiring users to pass
a slice to avoid the conversion, but I think that would inconvenience
users of this library unnecessarily.
Found by profiling the CPU time spent calculating quantiles in the
Prometheus Go client library.
Matt Bostock
5 years ago
76 | 76 | // is guaranteed to be within (Quantile±Epsilon). |
77 | 77 | // |
78 | 78 | // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. |
79 | func NewTargeted(targets map[float64]float64) *Stream { | |
79 | func NewTargeted(targetMap map[float64]float64) *Stream { | |
80 | // Convert map to slice to avoid slow iterations on a map. | |
81 | // ƒ is called on the hot path, so converting the map to a slice | |
82 | // beforehand results in significant CPU savings. | |
83 | targets := targetMapToSlice(targetMap) | |
84 | ||
80 | 85 | ƒ := func(s *stream, r float64) float64 { |
81 | 86 | var m = math.MaxFloat64 |
82 | 87 | var f float64 |
83 | for quantile, epsilon := range targets { | |
84 | if quantile*s.n <= r { | |
85 | f = (2 * epsilon * r) / quantile | |
88 | for _, t := range targets { | |
89 | if t.quantile*s.n <= r { | |
90 | f = (2 * t.epsilon * r) / t.quantile | |
86 | 91 | } else { |
87 | f = (2 * epsilon * (s.n - r)) / (1 - quantile) | |
92 | f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile) | |
88 | 93 | } |
89 | 94 | if f < m { |
90 | 95 | m = f |
93 | 98 | return m |
94 | 99 | } |
95 | 100 | return newStream(ƒ) |
101 | } | |
102 | ||
103 | type target struct { | |
104 | quantile float64 | |
105 | epsilon float64 | |
106 | } | |
107 | ||
108 | func targetMapToSlice(targetMap map[float64]float64) []target { | |
109 | targets := make([]target, 0, len(targetMap)) | |
110 | ||
111 | for quantile, epsilon := range targetMap { | |
112 | t := target{ | |
113 | quantile: quantile, | |
114 | epsilon: epsilon, | |
115 | } | |
116 | targets = append(targets, t) | |
117 | } | |
118 | ||
119 | return targets | |
96 | 120 | } |
97 | 121 | |
98 | 122 | // Stream computes quantiles for a stream of float64s. It is not thread-safe by |