Skip to content

Commit b4dcd1a

Browse files
Exca-DKExca-DKholiman
authored
metrics: make gauge_float64 and counter_float64 lock free (#27025)
Makes the float-gauges lock-free 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) --------- Co-authored-by: Exca-DK <[email protected]> Co-authored-by: Martin Holst Swende <[email protected]>
1 parent ab1a404 commit b4dcd1a

File tree

4 files changed

+74
-33
lines changed

4 files changed

+74
-33
lines changed

metrics/counter_float64.go

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package metrics
22

33
import (
4-
"sync"
4+
"math"
5+
"sync/atomic"
56
)
67

78
// CounterFloat64 holds a float64 value that can be incremented and decremented.
@@ -38,13 +39,13 @@ func NewCounterFloat64() CounterFloat64 {
3839
if !Enabled {
3940
return NilCounterFloat64{}
4041
}
41-
return &StandardCounterFloat64{count: 0.0}
42+
return &StandardCounterFloat64{}
4243
}
4344

4445
// NewCounterFloat64Forced constructs a new StandardCounterFloat64 and returns it no matter if
4546
// the global switch is enabled or not.
4647
func NewCounterFloat64Forced() CounterFloat64 {
47-
return &StandardCounterFloat64{count: 0.0}
48+
return &StandardCounterFloat64{}
4849
}
4950

5051
// NewRegisteredCounterFloat64 constructs and registers a new StandardCounterFloat64.
@@ -113,41 +114,42 @@ func (NilCounterFloat64) Inc(i float64) {}
113114
func (NilCounterFloat64) Snapshot() CounterFloat64 { return NilCounterFloat64{} }
114115

115116
// StandardCounterFloat64 is the standard implementation of a CounterFloat64 and uses the
116-
// sync.Mutex package to manage a single float64 value.
117+
// atomic to manage a single float64 value.
117118
type StandardCounterFloat64 struct {
118-
mutex sync.Mutex
119-
count float64
119+
floatBits atomic.Uint64
120120
}
121121

122122
// Clear sets the counter to zero.
123123
func (c *StandardCounterFloat64) Clear() {
124-
c.mutex.Lock()
125-
defer c.mutex.Unlock()
126-
c.count = 0.0
124+
c.floatBits.Store(0)
127125
}
128126

129127
// Count returns the current value.
130128
func (c *StandardCounterFloat64) Count() float64 {
131-
c.mutex.Lock()
132-
defer c.mutex.Unlock()
133-
return c.count
129+
return math.Float64frombits(c.floatBits.Load())
134130
}
135131

136132
// Dec decrements the counter by the given amount.
137133
func (c *StandardCounterFloat64) Dec(v float64) {
138-
c.mutex.Lock()
139-
defer c.mutex.Unlock()
140-
c.count -= v
134+
atomicAddFloat(&c.floatBits, -v)
141135
}
142136

143137
// Inc increments the counter by the given amount.
144138
func (c *StandardCounterFloat64) Inc(v float64) {
145-
c.mutex.Lock()
146-
defer c.mutex.Unlock()
147-
c.count += v
139+
atomicAddFloat(&c.floatBits, v)
148140
}
149141

150142
// Snapshot returns a read-only copy of the counter.
151143
func (c *StandardCounterFloat64) Snapshot() CounterFloat64 {
152144
return CounterFloat64Snapshot(c.Count())
153145
}
146+
147+
func atomicAddFloat(fbits *atomic.Uint64, v float64) {
148+
for {
149+
loadedBits := fbits.Load()
150+
newBits := math.Float64bits(math.Float64frombits(loadedBits) + v)
151+
if fbits.CompareAndSwap(loadedBits, newBits) {
152+
break
153+
}
154+
}
155+
}

metrics/counter_float_64_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package metrics
22

3-
import "testing"
3+
import (
4+
"sync"
5+
"testing"
6+
)
47

58
func BenchmarkCounterFloat64(b *testing.B) {
69
c := NewCounterFloat64()
@@ -10,6 +13,25 @@ func BenchmarkCounterFloat64(b *testing.B) {
1013
}
1114
}
1215

16+
func BenchmarkCounterFloat64Parallel(b *testing.B) {
17+
c := NewCounterFloat64()
18+
b.ResetTimer()
19+
var wg sync.WaitGroup
20+
for i := 0; i < 10; i++ {
21+
wg.Add(1)
22+
go func() {
23+
for i := 0; i < b.N; i++ {
24+
c.Inc(1.0)
25+
}
26+
wg.Done()
27+
}()
28+
}
29+
wg.Wait()
30+
if have, want := c.Count(), 10.0*float64(b.N); have != want {
31+
b.Fatalf("have %f want %f", have, want)
32+
}
33+
}
34+
1335
func TestCounterFloat64Clear(t *testing.T) {
1436
c := NewCounterFloat64()
1537
c.Inc(1.0)

metrics/gauge_float64.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package metrics
22

3-
import "sync"
3+
import (
4+
"math"
5+
"sync/atomic"
6+
)
47

58
// GaugeFloat64s hold a float64 value that can be set arbitrarily.
69
type GaugeFloat64 interface {
@@ -23,9 +26,7 @@ func NewGaugeFloat64() GaugeFloat64 {
2326
if !Enabled {
2427
return NilGaugeFloat64{}
2528
}
26-
return &StandardGaugeFloat64{
27-
value: 0.0,
28-
}
29+
return &StandardGaugeFloat64{}
2930
}
3031

3132
// NewRegisteredGaugeFloat64 constructs and registers a new StandardGaugeFloat64.
@@ -83,10 +84,9 @@ func (NilGaugeFloat64) Update(v float64) {}
8384
func (NilGaugeFloat64) Value() float64 { return 0.0 }
8485

8586
// StandardGaugeFloat64 is the standard implementation of a GaugeFloat64 and uses
86-
// sync.Mutex to manage a single float64 value.
87+
// atomic to manage a single float64 value.
8788
type StandardGaugeFloat64 struct {
88-
mutex sync.Mutex
89-
value float64
89+
floatBits atomic.Uint64
9090
}
9191

9292
// Snapshot returns a read-only copy of the gauge.
@@ -96,16 +96,12 @@ func (g *StandardGaugeFloat64) Snapshot() GaugeFloat64 {
9696

9797
// Update updates the gauge's value.
9898
func (g *StandardGaugeFloat64) Update(v float64) {
99-
g.mutex.Lock()
100-
defer g.mutex.Unlock()
101-
g.value = v
99+
g.floatBits.Store(math.Float64bits(v))
102100
}
103101

104102
// Value returns the gauge's current value.
105103
func (g *StandardGaugeFloat64) Value() float64 {
106-
g.mutex.Lock()
107-
defer g.mutex.Unlock()
108-
return g.value
104+
return math.Float64frombits(g.floatBits.Load())
109105
}
110106

111107
// FunctionalGaugeFloat64 returns value from given function

metrics/gauge_float64_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package metrics
22

3-
import "testing"
3+
import (
4+
"sync"
5+
"testing"
6+
)
47

58
func BenchmarkGaugeFloat64(b *testing.B) {
69
g := NewGaugeFloat64()
@@ -10,6 +13,24 @@ func BenchmarkGaugeFloat64(b *testing.B) {
1013
}
1114
}
1215

16+
func BenchmarkGaugeFloat64Parallel(b *testing.B) {
17+
c := NewGaugeFloat64()
18+
var wg sync.WaitGroup
19+
for i := 0; i < 10; i++ {
20+
wg.Add(1)
21+
go func() {
22+
for i := 0; i < b.N; i++ {
23+
c.Update(float64(i))
24+
}
25+
wg.Done()
26+
}()
27+
}
28+
wg.Wait()
29+
if have, want := c.Value(), float64(b.N-1); have != want {
30+
b.Fatalf("have %f want %f", have, want)
31+
}
32+
}
33+
1334
func TestGaugeFloat64(t *testing.T) {
1435
g := NewGaugeFloat64()
1536
g.Update(47.0)

0 commit comments

Comments
 (0)