github.com/zeebo/mon@v0.0.0-20211012163247-13d39bdb54fa/inthist/histogram.go (about) 1 package inthist 2 3 import ( 4 "math/bits" 5 "sync/atomic" 6 "unsafe" 7 8 "github.com/zeebo/mon/internal/bitmap" 9 "golang.org/x/sys/cpu" 10 ) 11 12 //go:noescape 13 func sumHistogramAVX2(data *[64]uint32) uint64 14 15 // sumHistogram is either backed by AVX2 or a partially unrolled loop. 16 var sumHistogram = map[bool]func(*[64]uint32) uint64{ 17 true: sumHistogramAVX2, 18 false: sumHistogramSlow, 19 }[cpu.X86.HasAVX2] 20 21 // sumHistogramSlow sums the histogram buffers using an unrolled loop. 22 func sumHistogramSlow(buf *[64]uint32) (total uint64) { 23 for i := 0; i <= 56; i += 8 { 24 total += 0 + 25 uint64(buf[i+0]) + 26 uint64(buf[i+1]) + 27 uint64(buf[i+2]) + 28 uint64(buf[i+3]) + 29 uint64(buf[i+4]) + 30 uint64(buf[i+5]) + 31 uint64(buf[i+6]) + 32 uint64(buf[i+7]) 33 } 34 return total 35 } 36 37 const ( // histEntriesBits of 6 keeps ~1.5% error. 38 histEntriesBits = 6 39 histBuckets = 63 - histEntriesBits 40 histEntries = 1 << histEntriesBits // 64 41 ) 42 43 // histBucket is the type of a histogram bucket. 44 type histBucket struct { 45 entries [histEntries]uint32 46 } 47 48 // loadBucket atomically loads the bucket pointer from the address. 49 func loadBucket(addr **histBucket) *histBucket { 50 return (*histBucket)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(addr)))) 51 } 52 53 // casBucket atomically compares and swaps the bucket pointer into the address. 54 func casBucket(addr **histBucket, old, new *histBucket) bool { 55 return atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(addr)), 56 unsafe.Pointer(old), unsafe.Pointer(new)) 57 } 58 59 // lowerValue returns the smallest value that can be stored at the entry. 60 func lowerValue(bucket, entry uint64) int64 { 61 return (1<<bucket-1)<<histEntriesBits + int64(entry<<bucket) 62 } 63 64 // upperValue returns the largest value that can be stored at the entry (inclusive). 65 func upperValue(bucket, entry uint64) int64 { 66 return (1<<bucket-1)<<histEntriesBits + int64(entry<<bucket) + (1<<bucket - 1) 67 } 68 69 // middleBase returns the base offset for finding the middleValue for a bucket. 70 func middleBase(bucket uint64) float64 { 71 return float64((int64(1)<<bucket-1)<<histEntriesBits) + 72 float64((int64(1)<<bucket)-1)/2 73 } 74 75 // middleOffset returns the amount to add to the appropriate middleBase to get the 76 // middleValue for some bucket and entry. 77 func middleOffset(bucket, entry uint64) float64 { 78 return float64(int64(entry << bucket)) 79 } 80 81 // bucketEntry returns the bucket and entry that should contain the value v. 82 func bucketEntry(v int64) (bucket, entry uint64) { 83 uv := uint64(v + histEntries) 84 bucket = uint64(bits.Len64(uv)) - histEntriesBits - 1 85 return bucket % 64, (uv>>bucket - histEntries) % histEntries 86 } 87 88 // Histogram keeps track of an exponentially increasing range of buckets 89 // so that there is a consistent relative error per bucket. 90 type Histogram struct { 91 bitmap bitmap.B64 // encodes which buckets are set 92 buckets [64]*histBucket // 64 so that bounds checks can be removed easier 93 } 94 95 // Observe records the value in the histogram. 96 func (h *Histogram) Observe(v int64) { 97 // upperValue is inlined and constant folded 98 if v < 0 || v > upperValue(histBuckets-1, histEntries-1) { 99 return 100 } 101 102 bucket, entry := bucketEntry(v) 103 104 b := loadBucket(&h.buckets[bucket]) 105 if b == nil { 106 b = new(histBucket) 107 if !casBucket(&h.buckets[bucket], nil, b) { 108 b = loadBucket(&h.buckets[bucket]) 109 } else { 110 h.bitmap.Set(uint(bucket)) 111 } 112 } 113 114 atomic.AddUint32(&b.entries[entry], 1) 115 } 116 117 // Total returns the number of completed calls. 118 func (h *Histogram) Total() (total int64) { 119 bm := h.bitmap.Clone() 120 for { 121 bucket, ok := bm.Next() 122 if !ok { 123 return total 124 } 125 total += int64(sumHistogram(&loadBucket(&h.buckets[bucket]).entries)) 126 } 127 } 128 129 // For quantile, we compute a target value at the start. After that, when 130 // walking the counts, we are sure we'll still hit the target since the 131 // counts and totals monotonically increase. This means that the returned 132 // result might be slightly smaller than the real result, but since 133 // the call is so fast, it's unlikely to drift very much. 134 135 // Quantile returns an estimation of the qth quantile in [0, 1]. 136 func (h *Histogram) Quantile(q float64) int64 { 137 target, acc := uint64(q*float64(h.Total())+0.5), uint64(0) 138 139 bm := h.bitmap.Clone() 140 for { 141 bucket, ok := bm.Next() 142 if !ok { 143 return upperValue(histBuckets-1, histEntries-1) 144 } 145 146 b := loadBucket(&h.buckets[bucket]) 147 bacc := acc + sumHistogram(&b.entries) 148 if bacc < target { 149 acc = bacc 150 continue 151 } 152 153 for entry := range b.entries[:] { 154 acc += uint64(atomic.LoadUint32(&b.entries[entry])) 155 if acc >= target { 156 base := middleBase(uint64(bucket)) 157 return int64(base + 0.5 + middleOffset(uint64(bucket), uint64(entry))) 158 } 159 } 160 } 161 } 162 163 // CDF returns an estimate for what quantile the value v is. 164 func (h *Histogram) CDF(v int64) float64 { 165 var sum, total int64 166 vbucket, ventry := bucketEntry(v) 167 bm := h.bitmap.Clone() 168 for { 169 bucket, ok := bm.Next() 170 if !ok { 171 return float64(sum) / float64(total) 172 } 173 174 entries := &loadBucket(&h.buckets[bucket]).entries 175 bucketSum := int64(sumHistogram(entries)) 176 total += bucketSum 177 178 if uint64(bucket) < vbucket { 179 sum += bucketSum 180 } else if uint64(bucket) == vbucket { 181 for i := uint64(0); i <= ventry; i++ { 182 sum += int64(entries[i]) 183 } 184 } 185 } 186 } 187 188 // When computing the average or variance, we don't do any locking. 189 // When we have finished adding up into the accumulator, we know the 190 // actual statistic has to be somewhere between acc / stotal and 191 // acc / etotal, because the counts and totals monotonically increase. 192 // We return the average of those bounds. Since we're dominated by 193 // cache misses, this doesn't cost much extra. 194 195 // Sum returns an estimation of the sum. 196 func (h *Histogram) Sum() float64 { 197 var values float64 198 199 bm := h.bitmap.Clone() 200 for { 201 bucket, ok := bm.Next() 202 if !ok { 203 return values 204 } 205 206 b := loadBucket(&h.buckets[bucket]) 207 base := middleBase(uint64(bucket)) 208 209 for entry := range b.entries[:] { 210 if count := float64(atomic.LoadUint32(&b.entries[entry])); count > 0 { 211 value := base + middleOffset(uint64(bucket), uint64(entry)) 212 values += count * value 213 } 214 } 215 } 216 } 217 218 // Average returns an estimation of the sum and average. 219 func (h *Histogram) Average() (float64, float64) { 220 var values, total float64 221 222 bm := h.bitmap.Clone() 223 for { 224 bucket, ok := bm.Next() 225 if !ok { 226 if total == 0 { 227 return 0, 0 228 } 229 return values, values / total 230 } 231 232 b := loadBucket(&h.buckets[bucket]) 233 base := middleBase(uint64(bucket)) 234 235 for entry := range b.entries[:] { 236 if count := float64(atomic.LoadUint32(&b.entries[entry])); count > 0 { 237 value := base + middleOffset(uint64(bucket), uint64(entry)) 238 values += count * value 239 total += count 240 } 241 } 242 } 243 } 244 245 // Variance returns an estimation of the sum, average and variance. 246 func (h *Histogram) Variance() (float64, float64, float64) { 247 var values, total, total2, mean, vari float64 248 249 bm := h.bitmap.Clone() 250 for { 251 bucket, ok := bm.Next() 252 if !ok { 253 if total == 0 { 254 return 0, 0, 0 255 } 256 return values, values / total, vari / total 257 } 258 259 b := loadBucket(&h.buckets[bucket]) 260 base := middleBase(uint64(bucket)) 261 262 for entry := range b.entries[:] { 263 if count := float64(atomic.LoadUint32(&b.entries[entry])); count > 0 { 264 value := base + middleOffset(uint64(bucket), uint64(entry)) 265 values += count * value 266 total += count 267 total2 += count * count 268 mean_ := mean 269 mean += (count / total) * (value - mean_) 270 vari += count * (value - mean_) * (value - mean) 271 } 272 } 273 } 274 } 275 276 // Percentiles returns calls the callback with information about the CDF. 277 // The total may increase during the call, but it should never be less 278 // than the count. 279 func (h *Histogram) Percentiles(cb func(value, count, total int64)) { 280 acc, total := int64(0), h.Total() 281 282 bm := h.bitmap.Clone() 283 for { 284 bucket, ok := bm.Next() 285 if !ok { 286 return 287 } 288 289 b := loadBucket(&h.buckets[bucket]) 290 for entry := range b.entries[:] { 291 if count := int64(atomic.LoadUint32(&b.entries[entry])); count > 0 { 292 if acc == 0 { 293 cb(lowerValue(uint64(bucket), uint64(entry)), 0, total) 294 } 295 acc += count 296 if acc > total { 297 total = h.Total() 298 } 299 cb(upperValue(uint64(bucket), uint64(entry)), acc, total) 300 } 301 } 302 } 303 }