From ec7e2edd642848f6baa7c2e10b937e2f2d1d0dd3 Mon Sep 17 00:00:00 2001 From: Exca-DK Date: Sat, 1 Apr 2023 17:20:18 +0200 Subject: [PATCH 1/6] mutex-free gaugef64 and counterf64 --- metrics/counter_float64.go | 48 +++++++++++++++++++++----------- metrics/counter_float_64_test.go | 19 ++++++++++++- metrics/gauge_float64.go | 17 +++-------- metrics/gauge_float64_test.go | 19 ++++++++++++- 4 files changed, 71 insertions(+), 32 deletions(-) diff --git a/metrics/counter_float64.go b/metrics/counter_float64.go index f05f62fed632..6cd6e143802c 100644 --- a/metrics/counter_float64.go +++ b/metrics/counter_float64.go @@ -1,7 +1,8 @@ package metrics import ( - "sync" + "math" + "sync/atomic" ) // CounterFloat64 holds a float64 value that can be incremented and decremented. @@ -38,13 +39,13 @@ func NewCounterFloat64() CounterFloat64 { if !Enabled { return NilCounterFloat64{} } - return &StandardCounterFloat64{count: 0.0} + return &StandardCounterFloat64{} } // NewCounterFloat64Forced constructs a new StandardCounterFloat64 and returns it no matter if // the global switch is enabled or not. func NewCounterFloat64Forced() CounterFloat64 { - return &StandardCounterFloat64{count: 0.0} + return &StandardCounterFloat64{} } // NewRegisteredCounterFloat64 constructs and registers a new StandardCounterFloat64. @@ -115,39 +116,52 @@ func (NilCounterFloat64) Snapshot() CounterFloat64 { return NilCounterFloat64{} // StandardCounterFloat64 is the standard implementation of a CounterFloat64 and uses the // sync.Mutex package to manage a single float64 value. type StandardCounterFloat64 struct { - mutex sync.Mutex - count float64 + floatBits uint64 } // Clear sets the counter to zero. func (c *StandardCounterFloat64) Clear() { - c.mutex.Lock() - defer c.mutex.Unlock() - c.count = 0.0 + atomicClearFloat(&c.floatBits) } // Count returns the current value. func (c *StandardCounterFloat64) Count() float64 { - c.mutex.Lock() - defer c.mutex.Unlock() - return c.count + return atomicGetFloat(&c.floatBits) } // Dec decrements the counter by the given amount. func (c *StandardCounterFloat64) Dec(v float64) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.count -= v + atomicAddFloat(&c.floatBits, -v) } // Inc increments the counter by the given amount. func (c *StandardCounterFloat64) Inc(v float64) { - c.mutex.Lock() - defer c.mutex.Unlock() - c.count += v + atomicAddFloat(&c.floatBits, v) } // Snapshot returns a read-only copy of the counter. func (c *StandardCounterFloat64) Snapshot() CounterFloat64 { return CounterFloat64Snapshot(c.Count()) } + +func atomicAddFloat(fbits *uint64, v float64) { + for { + loadedBits := atomic.LoadUint64(fbits) + newBits := math.Float64bits(math.Float64frombits(loadedBits) + v) + if atomic.CompareAndSwapUint64(fbits, loadedBits, newBits) { + break + } + } +} + +func atomicGetFloat(addr *uint64) float64 { + return math.Float64frombits(atomic.LoadUint64(addr)) +} + +func atomicClearFloat(addr *uint64) { + atomic.StoreUint64(addr, 0) +} + +func atomicStoreFloat(addr *uint64, v float64) { + atomic.StoreUint64(addr, math.Float64bits(v)) +} diff --git a/metrics/counter_float_64_test.go b/metrics/counter_float_64_test.go index 44d9b4c20c85..800f7bec8fb4 100644 --- a/metrics/counter_float_64_test.go +++ b/metrics/counter_float_64_test.go @@ -1,6 +1,9 @@ package metrics -import "testing" +import ( + "sync" + "testing" +) func BenchmarkCounterFloat64(b *testing.B) { c := NewCounterFloat64() @@ -10,6 +13,20 @@ func BenchmarkCounterFloat64(b *testing.B) { } } +func BenchmarkCounterFloat64Pararel(b *testing.B) { + c := NewCounterFloat64() + b.ResetTimer() + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + go func(i float64) { + c.Inc(i) + wg.Done() + }(float64(i)) + } + wg.Wait() +} + func TestCounterFloat64Clear(t *testing.T) { c := NewCounterFloat64() c.Inc(1.0) diff --git a/metrics/gauge_float64.go b/metrics/gauge_float64.go index 66819c957774..4215da8d97e1 100644 --- a/metrics/gauge_float64.go +++ b/metrics/gauge_float64.go @@ -1,7 +1,5 @@ package metrics -import "sync" - // GaugeFloat64s hold a float64 value that can be set arbitrarily. type GaugeFloat64 interface { Snapshot() GaugeFloat64 @@ -23,9 +21,7 @@ func NewGaugeFloat64() GaugeFloat64 { if !Enabled { return NilGaugeFloat64{} } - return &StandardGaugeFloat64{ - value: 0.0, - } + return &StandardGaugeFloat64{} } // NewRegisteredGaugeFloat64 constructs and registers a new StandardGaugeFloat64. @@ -85,8 +81,7 @@ func (NilGaugeFloat64) Value() float64 { return 0.0 } // StandardGaugeFloat64 is the standard implementation of a GaugeFloat64 and uses // sync.Mutex to manage a single float64 value. type StandardGaugeFloat64 struct { - mutex sync.Mutex - value float64 + floatBits uint64 } // Snapshot returns a read-only copy of the gauge. @@ -96,16 +91,12 @@ func (g *StandardGaugeFloat64) Snapshot() GaugeFloat64 { // Update updates the gauge's value. func (g *StandardGaugeFloat64) Update(v float64) { - g.mutex.Lock() - defer g.mutex.Unlock() - g.value = v + atomicStoreFloat(&g.floatBits, v) } // Value returns the gauge's current value. func (g *StandardGaugeFloat64) Value() float64 { - g.mutex.Lock() - defer g.mutex.Unlock() - return g.value + return atomicGetFloat(&g.floatBits) } // FunctionalGaugeFloat64 returns value from given function diff --git a/metrics/gauge_float64_test.go b/metrics/gauge_float64_test.go index 7b854d232ba8..98bc25e65e80 100644 --- a/metrics/gauge_float64_test.go +++ b/metrics/gauge_float64_test.go @@ -1,6 +1,9 @@ package metrics -import "testing" +import ( + "sync" + "testing" +) func BenchmarkGaugeFloat64(b *testing.B) { g := NewGaugeFloat64() @@ -10,6 +13,20 @@ func BenchmarkGaugeFloat64(b *testing.B) { } } +func BenchmarkGaugeFloat64Pararel(b *testing.B) { + c := NewGaugeFloat64() + b.ResetTimer() + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + go func(i float64) { + c.Update(i) + wg.Done() + }(float64(i)) + } + wg.Wait() +} + func TestGaugeFloat64(t *testing.T) { g := NewGaugeFloat64() g.Update(47.0) From 48f7a86505ac379815093f07520f37e80d27cd56 Mon Sep 17 00:00:00 2001 From: Exca-DK Date: Sun, 2 Apr 2023 12:56:34 +0200 Subject: [PATCH 2/6] fix typos --- metrics/counter_float64.go | 2 +- metrics/counter_float_64_test.go | 2 +- metrics/gauge_float64.go | 2 +- metrics/gauge_float64_test.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/metrics/counter_float64.go b/metrics/counter_float64.go index 6cd6e143802c..e13631bf3b96 100644 --- a/metrics/counter_float64.go +++ b/metrics/counter_float64.go @@ -114,7 +114,7 @@ func (NilCounterFloat64) Inc(i float64) {} func (NilCounterFloat64) Snapshot() CounterFloat64 { return NilCounterFloat64{} } // StandardCounterFloat64 is the standard implementation of a CounterFloat64 and uses the -// sync.Mutex package to manage a single float64 value. +// atomic to manage a single float64 value. type StandardCounterFloat64 struct { floatBits uint64 } diff --git a/metrics/counter_float_64_test.go b/metrics/counter_float_64_test.go index 800f7bec8fb4..0aeb551f89bd 100644 --- a/metrics/counter_float_64_test.go +++ b/metrics/counter_float_64_test.go @@ -13,7 +13,7 @@ func BenchmarkCounterFloat64(b *testing.B) { } } -func BenchmarkCounterFloat64Pararel(b *testing.B) { +func BenchmarkCounterFloat64Parallel(b *testing.B) { c := NewCounterFloat64() b.ResetTimer() var wg sync.WaitGroup diff --git a/metrics/gauge_float64.go b/metrics/gauge_float64.go index 4215da8d97e1..8b8c7f9a9ea4 100644 --- a/metrics/gauge_float64.go +++ b/metrics/gauge_float64.go @@ -79,7 +79,7 @@ func (NilGaugeFloat64) Update(v float64) {} func (NilGaugeFloat64) Value() float64 { return 0.0 } // StandardGaugeFloat64 is the standard implementation of a GaugeFloat64 and uses -// sync.Mutex to manage a single float64 value. +// atomic to manage a single float64 value. type StandardGaugeFloat64 struct { floatBits uint64 } diff --git a/metrics/gauge_float64_test.go b/metrics/gauge_float64_test.go index 98bc25e65e80..16ca9b88f78b 100644 --- a/metrics/gauge_float64_test.go +++ b/metrics/gauge_float64_test.go @@ -13,7 +13,7 @@ func BenchmarkGaugeFloat64(b *testing.B) { } } -func BenchmarkGaugeFloat64Pararel(b *testing.B) { +func BenchmarkGaugeFloat64Parallel(b *testing.B) { c := NewGaugeFloat64() b.ResetTimer() var wg sync.WaitGroup From b885cf83e405d24689093190b78e09b304f3e818 Mon Sep 17 00:00:00 2001 From: Exca-DK Date: Mon, 3 Apr 2023 12:30:58 +0200 Subject: [PATCH 3/6] atomic.Uint64 and parallel benchmarks less noisy --- metrics/counter_float64.go | 20 ++++++++++---------- metrics/counter_float_64_test.go | 15 +++++---------- metrics/gauge_float64.go | 4 +++- metrics/gauge_float64_test.go | 17 +++++++---------- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/metrics/counter_float64.go b/metrics/counter_float64.go index e13631bf3b96..4f25a58430af 100644 --- a/metrics/counter_float64.go +++ b/metrics/counter_float64.go @@ -116,7 +116,7 @@ func (NilCounterFloat64) Snapshot() CounterFloat64 { return NilCounterFloat64{} // StandardCounterFloat64 is the standard implementation of a CounterFloat64 and uses the // atomic to manage a single float64 value. type StandardCounterFloat64 struct { - floatBits uint64 + floatBits atomic.Uint64 } // Clear sets the counter to zero. @@ -144,24 +144,24 @@ func (c *StandardCounterFloat64) Snapshot() CounterFloat64 { return CounterFloat64Snapshot(c.Count()) } -func atomicAddFloat(fbits *uint64, v float64) { +func atomicAddFloat(fbits *atomic.Uint64, v float64) { for { - loadedBits := atomic.LoadUint64(fbits) + loadedBits := fbits.Load() newBits := math.Float64bits(math.Float64frombits(loadedBits) + v) - if atomic.CompareAndSwapUint64(fbits, loadedBits, newBits) { + if fbits.CompareAndSwap(loadedBits, newBits) { break } } } -func atomicGetFloat(addr *uint64) float64 { - return math.Float64frombits(atomic.LoadUint64(addr)) +func atomicGetFloat(addr *atomic.Uint64) float64 { + return math.Float64frombits(addr.Load()) } -func atomicClearFloat(addr *uint64) { - atomic.StoreUint64(addr, 0) +func atomicClearFloat(addr *atomic.Uint64) { + addr.Store(0) } -func atomicStoreFloat(addr *uint64, v float64) { - atomic.StoreUint64(addr, math.Float64bits(v)) +func atomicStoreFloat(addr *atomic.Uint64, v float64) { + addr.Store(math.Float64bits(v)) } diff --git a/metrics/counter_float_64_test.go b/metrics/counter_float_64_test.go index 0aeb551f89bd..8653c0eec944 100644 --- a/metrics/counter_float_64_test.go +++ b/metrics/counter_float_64_test.go @@ -1,7 +1,6 @@ package metrics import ( - "sync" "testing" ) @@ -16,15 +15,11 @@ func BenchmarkCounterFloat64(b *testing.B) { func BenchmarkCounterFloat64Parallel(b *testing.B) { c := NewCounterFloat64() b.ResetTimer() - var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - go func(i float64) { - c.Inc(i) - wg.Done() - }(float64(i)) - } - wg.Wait() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + c.Inc(1.0) + } + }) } func TestCounterFloat64Clear(t *testing.T) { diff --git a/metrics/gauge_float64.go b/metrics/gauge_float64.go index 8b8c7f9a9ea4..4a2f089ab92f 100644 --- a/metrics/gauge_float64.go +++ b/metrics/gauge_float64.go @@ -1,5 +1,7 @@ package metrics +import "sync/atomic" + // GaugeFloat64s hold a float64 value that can be set arbitrarily. type GaugeFloat64 interface { Snapshot() GaugeFloat64 @@ -81,7 +83,7 @@ func (NilGaugeFloat64) Value() float64 { return 0.0 } // StandardGaugeFloat64 is the standard implementation of a GaugeFloat64 and uses // atomic to manage a single float64 value. type StandardGaugeFloat64 struct { - floatBits uint64 + floatBits atomic.Uint64 } // Snapshot returns a read-only copy of the gauge. diff --git a/metrics/gauge_float64_test.go b/metrics/gauge_float64_test.go index 16ca9b88f78b..b2d10b8d0828 100644 --- a/metrics/gauge_float64_test.go +++ b/metrics/gauge_float64_test.go @@ -1,7 +1,6 @@ package metrics import ( - "sync" "testing" ) @@ -16,15 +15,13 @@ func BenchmarkGaugeFloat64(b *testing.B) { func BenchmarkGaugeFloat64Parallel(b *testing.B) { c := NewGaugeFloat64() b.ResetTimer() - var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - go func(i float64) { - c.Update(i) - wg.Done() - }(float64(i)) - } - wg.Wait() + b.RunParallel(func(pb *testing.PB) { + count := 1.0 + for pb.Next() { + c.Update(count) + count += 1.0 + } + }) } func TestGaugeFloat64(t *testing.T) { From 447a032337819d25156a5ddebed01c0861d39ed5 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 3 Apr 2023 12:49:45 +0200 Subject: [PATCH 4/6] metrics: slight refactor --- metrics/counter_float64.go | 16 ++-------------- metrics/gauge_float64.go | 9 ++++++--- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/metrics/counter_float64.go b/metrics/counter_float64.go index 4f25a58430af..d1197bb8e0ae 100644 --- a/metrics/counter_float64.go +++ b/metrics/counter_float64.go @@ -121,12 +121,12 @@ type StandardCounterFloat64 struct { // Clear sets the counter to zero. func (c *StandardCounterFloat64) Clear() { - atomicClearFloat(&c.floatBits) + c.floatBits.Store(0) } // Count returns the current value. func (c *StandardCounterFloat64) Count() float64 { - return atomicGetFloat(&c.floatBits) + return math.Float64frombits(c.floatBits.Load()) } // Dec decrements the counter by the given amount. @@ -153,15 +153,3 @@ func atomicAddFloat(fbits *atomic.Uint64, v float64) { } } } - -func atomicGetFloat(addr *atomic.Uint64) float64 { - return math.Float64frombits(addr.Load()) -} - -func atomicClearFloat(addr *atomic.Uint64) { - addr.Store(0) -} - -func atomicStoreFloat(addr *atomic.Uint64, v float64) { - addr.Store(math.Float64bits(v)) -} diff --git a/metrics/gauge_float64.go b/metrics/gauge_float64.go index 4a2f089ab92f..237ff8036e01 100644 --- a/metrics/gauge_float64.go +++ b/metrics/gauge_float64.go @@ -1,6 +1,9 @@ package metrics -import "sync/atomic" +import ( + "math" + "sync/atomic" +) // GaugeFloat64s hold a float64 value that can be set arbitrarily. type GaugeFloat64 interface { @@ -93,12 +96,12 @@ func (g *StandardGaugeFloat64) Snapshot() GaugeFloat64 { // Update updates the gauge's value. func (g *StandardGaugeFloat64) Update(v float64) { - atomicStoreFloat(&g.floatBits, v) + g.floatBits.Store(math.Float64bits(v)) } // Value returns the gauge's current value. func (g *StandardGaugeFloat64) Value() float64 { - return atomicGetFloat(&g.floatBits) + return math.Float64frombits(g.floatBits.Load()) } // FunctionalGaugeFloat64 returns value from given function From 56c4c2b6de32619e7d115a47bf210bb0e8037714 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 3 Apr 2023 12:58:22 +0200 Subject: [PATCH 5/6] metrics: make benchmark more realistic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit name old time/op new time/op delta CounterFloat64Parallel-8 1.45µs ±10% 0.85µs ± 6% -41.65% (p=0.008 n=5+5) --- metrics/counter_float_64_test.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/metrics/counter_float_64_test.go b/metrics/counter_float_64_test.go index 8653c0eec944..f17aca330cbe 100644 --- a/metrics/counter_float_64_test.go +++ b/metrics/counter_float_64_test.go @@ -1,6 +1,7 @@ package metrics import ( + "sync" "testing" ) @@ -15,11 +16,20 @@ func BenchmarkCounterFloat64(b *testing.B) { func BenchmarkCounterFloat64Parallel(b *testing.B) { c := NewCounterFloat64() b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - c.Inc(1.0) - } - }) + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + for i := 0; i < b.N; i++ { + c.Inc(1.0) + } + wg.Done() + }() + } + wg.Wait() + if have, want := c.Count(), 10.0*float64(b.N); have != want { + b.Fatalf("have %f want %f", have, want) + } } func TestCounterFloat64Clear(t *testing.T) { From f93e4931750592b761ef75918f014d515e0c2567 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Mon, 3 Apr 2023 13:05:45 +0200 Subject: [PATCH 6/6] squashme --- metrics/gauge_float64_test.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/metrics/gauge_float64_test.go b/metrics/gauge_float64_test.go index b2d10b8d0828..647d09000935 100644 --- a/metrics/gauge_float64_test.go +++ b/metrics/gauge_float64_test.go @@ -1,6 +1,7 @@ package metrics import ( + "sync" "testing" ) @@ -14,14 +15,20 @@ func BenchmarkGaugeFloat64(b *testing.B) { func BenchmarkGaugeFloat64Parallel(b *testing.B) { c := NewGaugeFloat64() - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - count := 1.0 - for pb.Next() { - c.Update(count) - count += 1.0 - } - }) + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + for i := 0; i < b.N; i++ { + c.Update(float64(i)) + } + wg.Done() + }() + } + wg.Wait() + if have, want := c.Value(), float64(b.N-1); have != want { + b.Fatalf("have %f want %f", have, want) + } } func TestGaugeFloat64(t *testing.T) {