Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion histogram.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ type Histogram struct {
// ZeroCount counts exact zeros (and negative values).
ZeroCount atomic.Uint64
sum atomic.Int64 // using Int64 since CockroachDB histograms record int64

// min/max track the exact smallest and largest values recorded, for
// the perf-eval of exact extreme tracking. Updated only by the
// RecordMinMax* variants. min is seeded to MaxInt64 and max to
// MinInt64 so the first observation always wins.
min atomic.Int64
max atomic.Int64

// minP/maxP are cache-line-padded variants of min/max, used by
// RecordMinMaxPadded to isolate the false-sharing cost of placing the
// extremes next to the hot sum counter on the same cache line.
minP paddedInt64
maxP paddedInt64
}

// paddedInt64 is an atomic.Int64 padded to a full 64-byte cache line so
// that updates to it do not invalidate neighbouring fields (false sharing).
type paddedInt64 struct {
v atomic.Int64
_ [56]byte // 64 - 8 bytes
}

// Reset zeroes all counters without reallocating the backing slice.
Expand All @@ -325,10 +345,15 @@ func (h *Histogram) Reset() {
func New(p Params) *Histogram {
p = p.withDefaults()
cfg := getOrCreateConfig(p)
return &Histogram{
h := &Histogram{
cfg: cfg,
counts: make([]atomic.Uint64, cfg.numBuckets),
}
h.min.Store(math.MaxInt64)
h.max.Store(math.MinInt64)
h.minP.v.Store(math.MaxInt64)
h.maxP.v.Store(math.MinInt64)
return h
}

// Record adds a value to the histogram. This is the hot path: O(1), lock-free,
Expand Down
74 changes: 74 additions & 0 deletions minmax.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2026 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0

package goodhistogram

import "sync/atomic"

// This file holds experimental variants of Record that additionally track the
// exact minimum and maximum values observed. They exist purely for the
// performance evaluation of exact extreme tracking on the lock-free hot path
// and are compared A/B against the baseline Record in minmax_benchmark_test.go.
//
// Tracking exact extremes cannot use a fetch-and-add like the bucket counters
// and sum; it needs a compare-and-swap loop guarded by a relaxed load. The
// load short-circuits the common steady-state case (the value is not a new
// extreme), so the CAS only fires when a new extreme is actually seen. The
// interesting cost is therefore (a) two extra shared-atomic loads on every
// Record and (b) contended CAS retries when extremes churn.

// updateMin lowers dst toward v using a load-guarded CAS loop.
func updateMin(dst *atomic.Int64, v int64) {
for {
old := dst.Load()
if v >= old {
return
}
if dst.CompareAndSwap(old, v) {
return
}
}
}

// updateMax raises dst toward v using a load-guarded CAS loop.
func updateMax(dst *atomic.Int64, v int64) {
for {
old := dst.Load()
if v <= old {
return
}
if dst.CompareAndSwap(old, v) {
return
}
}
}

// RecordMinMax records v and additionally tracks the exact min and max in two
// atomics packed inline in the Histogram struct (adjacent to sum, so they may
// share a cache line with the other hot counters).
func (h *Histogram) RecordMinMax(v int64) {
updateMin(&h.min, v)
updateMax(&h.max, v)
h.Record(v)
}

// RecordMinMaxPadded records v and tracks exact min and max in cache-line
// padded atomics, isolating the false-sharing cost from the inline variant.
func (h *Histogram) RecordMinMaxPadded(v int64) {
updateMin(&h.minP.v, v)
updateMax(&h.maxP.v, v)
h.Record(v)
}

// Min returns the exact minimum recorded via a RecordMinMax variant, or
// MaxInt64 if none.
func (h *Histogram) Min() int64 { return h.min.Load() }

// Max returns the exact maximum recorded via a RecordMinMax variant, or
// MinInt64 if none.
func (h *Histogram) Max() int64 { return h.max.Load() }
122 changes: 122 additions & 0 deletions minmax_benchmark_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2026 The Cockroach Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0

package goodhistogram

import (
"fmt"
"math/rand"
"sort"
"testing"
)

// --------------------------------------------------------------------------
// Performance evaluation: exact min/max tracking on the hot Record path.
//
// Baseline is the existing Record. The variants add a load-guarded CAS loop
// for both the exact min and the exact max:
//
// record - baseline, no extreme tracking
// record-minmax - min/max in atomics packed inline next to sum
// record-padded - min/max in cache-line-padded atomics (no false sharing)
//
// The cost of extreme tracking depends entirely on how often a new extreme is
// seen, which is a property of input *ordering*, not the distribution's shape:
//
// steady - shuffled log-uniform values. After a brief warm-up the extremes
// stop moving, so every CAS is skipped by the guard load. This is
// the common metrics case (latencies bounce around a stable range).
// ascending- monotonically increasing values: every single Record sets a new
// max, forcing a CAS on every call. Adversarial worst case.
// descending- monotonically decreasing: every Record sets a new min.
// --------------------------------------------------------------------------

type ordering struct {
name string
gen func(rng *rand.Rand, n int) []int64
}

var minMaxOrderings = []ordering{
{
name: "steady",
gen: func(rng *rand.Rand, n int) []int64 {
return makeInt64Values(rng, n) // shuffled log-uniform
},
},
{
name: "ascending",
gen: func(rng *rand.Rand, n int) []int64 {
v := makeInt64Values(rng, n)
sort.Slice(v, func(i, j int) bool { return v[i] < v[j] })
return v
},
},
{
name: "descending",
gen: func(rng *rand.Rand, n int) []int64 {
v := makeInt64Values(rng, n)
sort.Slice(v, func(i, j int) bool { return v[i] > v[j] })
return v
},
},
}

// recordVariants maps a name to the record method under test.
var recordVariants = []struct {
name string
fn func(h *Histogram, v int64)
}{
{"record", (*Histogram).Record},
{"record-minmax", (*Histogram).RecordMinMax},
{"record-padded", (*Histogram).RecordMinMaxPadded},
}

// BenchmarkMinMaxSingleThread measures per-Record cost single-threaded, across
// input orderings. Isolates the raw instruction/CAS cost with no contention.
func BenchmarkMinMaxSingleThread(b *testing.B) {
const nVals = 100_000
for _, ord := range minMaxOrderings {
vals := ord.gen(rand.New(rand.NewSource(42)), nVals)
for _, variant := range recordVariants {
b.Run(fmt.Sprintf("order=%s/%s", ord.name, variant.name), func(b *testing.B) {
h := newGoodHist()
b.ResetTimer()
for i := 0; i < b.N; i++ {
variant.fn(h, vals[i%len(vals)])
}
})
}
}
}

// BenchmarkMinMaxContention measures per-Record cost under contention. This is
// where extreme tracking is expected to hurt most: the shared min/max atomics
// bounce between cores' caches even when the guard load skips the CAS, and
// under the ascending/descending orderings the CAS itself is heavily contended.
func BenchmarkMinMaxContention(b *testing.B) {
const nVals = 100_000
for _, numG := range []int{50, 100} {
for _, ord := range minMaxOrderings {
vals := ord.gen(rand.New(rand.NewSource(42)), nVals)
for _, variant := range recordVariants {
b.Run(fmt.Sprintf("g=%d/order=%s/%s", numG, ord.name, variant.name), func(b *testing.B) {
h := newGoodHist()
b.SetParallelism(numG)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
variant.fn(h, vals[i%len(vals)])
i++
}
})
})
}
}
}
}
33 changes: 33 additions & 0 deletions reports/minmax_benchstat_amd64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
x86_64 GCE worker (24 vCPU), Go 1.25.5
Single-threaded (sec/op, vs baseline):
│ sec/op │ sec/op vs base │ sec/op vs base │
MinMaxSingleThread/order=steady-24 20.86n ± 0% 23.00n ± 0% +10.31% (p=0.000 n=8) 22.71n ± 0% +8.92% (p=0.000 n=8)
MinMaxSingleThread/order=ascending-24 20.86n ± 0% 22.98n ± 0% +10.16% (p=0.000 n=8) 22.68n ± 0% +8.70% (p=0.000 n=8)
MinMaxSingleThread/order=descending-24 20.88n ± 0% 23.02n ± 0% +10.28% (p=0.000 n=8) 22.67n ± 0% +8.57% (p=0.000 n=8)
MinMaxSingleThread/order=steady-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=ascending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=descending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=steady-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=ascending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=descending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹

Contention (sec/op, vs baseline):
│ sec/op │ sec/op vs base │ sec/op vs base │
MinMaxContention/g=50/order=steady-24 39.70n ± 2% 43.83n ± 1% +10.39% (p=0.000 n=8) 44.18n ± 4% +11.28% (p=0.000 n=8)
MinMaxContention/g=50/order=ascending-24 39.00n ± 42% 43.16n ± 1% +10.67% (p=0.000 n=8) 43.38n ± 7% +11.23% (p=0.000 n=8)
MinMaxContention/g=50/order=descending-24 38.91n ± 42% 42.97n ± 1% +10.45% (p=0.000 n=8) 43.22n ± 6% +11.08% (p=0.000 n=8)
MinMaxContention/g=100/order=steady-24 39.57n ± 1% 43.68n ± 1% +10.40% (p=0.000 n=8) 42.93n ± 7% +8.50% (p=0.000 n=8)
MinMaxContention/g=100/order=ascending-24 39.29n ± 2% 42.75n ± 2% +8.81% (p=0.000 n=8) 44.46n ± 10% +13.17% (p=0.000 n=8)
MinMaxContention/g=100/order=descending-24 38.27n ± 40% 42.78n ± 2% +11.77% (p=0.000 n=8) 44.74n ± 11% +16.90% (p=0.000 n=8)
MinMaxContention/g=50/order=steady-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=ascending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=descending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=steady-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=ascending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=descending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=steady-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=ascending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=descending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=steady-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=ascending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=descending-24 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
42 changes: 42 additions & 0 deletions reports/minmax_benchstat_arm64.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Single-threaded (sec/op, vs baseline):
│ SingleThread_baseline.txt │ SingleThread_minmax.txt │ SingleThread_padded.txt │
│ sec/op │ sec/op vs base │ sec/op vs base │
MinMaxSingleThread/order=steady-11 2.751n ± 1% 3.456n ± 1% +25.63% (p=0.000 n=8) 3.446n ± 7% +25.25% (p=0.000 n=8)
MinMaxSingleThread/order=ascending-11 2.690n ± 5% 3.488n ± 1% +29.65% (p=0.000 n=8) 4.941n ± 30% +83.70% (p=0.000 n=8)
MinMaxSingleThread/order=descending-11 3.827n ± 30% 3.504n ± 1% ~ (p=0.105 n=8) 4.610n ± 25% +20.46% (p=0.005 n=8)
│ SingleThread_baseline.txt │ SingleThread_minmax.txt │ SingleThread_padded.txt │
│ B/op │ B/op vs base │ B/op vs base │
MinMaxSingleThread/order=steady-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=ascending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=descending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
│ SingleThread_baseline.txt │ SingleThread_minmax.txt │ SingleThread_padded.txt │
│ allocs/op │ allocs/op vs base │ allocs/op vs base │
MinMaxSingleThread/order=steady-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=ascending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxSingleThread/order=descending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹

Contention (sec/op, vs baseline):
│ Contention_baseline.txt │ Contention_minmax.txt │ Contention_padded.txt │
│ sec/op │ sec/op vs base │ sec/op vs base │
MinMaxContention/g=50/order=steady-11 43.44n ± 13% 51.82n ± 27% +19.29% (p=0.007 n=8) 54.88n ± 13% +26.35% (p=0.000 n=8)
MinMaxContention/g=50/order=ascending-11 51.12n ± 4% 49.65n ± 12% ~ (p=0.645 n=8) 57.22n ± 18% +11.93% (p=0.010 n=8)
MinMaxContention/g=50/order=descending-11 38.88n ± 27% 36.49n ± 15% ~ (p=0.645 n=8) 60.43n ± 10% +55.43% (p=0.000 n=8)
MinMaxContention/g=100/order=steady-11 54.62n ± 6% 52.15n ± 26% ~ (p=0.645 n=8) 55.14n ± 20% ~ (p=0.442 n=8)
MinMaxContention/g=100/order=ascending-11 55.24n ± 8% 53.03n ± 6% ~ (p=0.342 n=8) 60.41n ± 23% ~ (p=0.130 n=8)
MinMaxContention/g=100/order=descending-11 41.14n ± 19% 37.01n ± 11% ~ (p=0.442 n=8) 63.93n ± 29% +55.38% (p=0.001 n=8)
│ Contention_baseline.txt │ Contention_minmax.txt │ Contention_padded.txt │
│ B/op │ B/op vs base │ B/op vs base │
MinMaxContention/g=50/order=steady-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=ascending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=descending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=steady-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=ascending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=descending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
│ Contention_baseline.txt │ Contention_minmax.txt │ Contention_padded.txt │
│ allocs/op │ allocs/op vs base │ allocs/op vs base │
MinMaxContention/g=50/order=steady-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=ascending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=50/order=descending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=steady-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=ascending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
MinMaxContention/g=100/order=descending-11 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=8) ¹ 0.000 ± 0% ~ (p=1.000 n=8) ¹
Loading
Loading