Skip to content
This repository was archived by the owner on Apr 1, 2025. It is now read-only.

Commit b08b742

Browse files
author
Ralph Caraveo
committed
Optimizes ewma to reduce lock contention
1 parent 0201454 commit b08b742

File tree

2 files changed

+45
-13
lines changed

2 files changed

+45
-13
lines changed

ewma.go

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,15 @@ func (NilEWMA) Update(n int64) {}
7979
type StandardEWMA struct {
8080
uncounted int64 // /!\ this should be the first member to ensure 64-bit alignment
8181
alpha float64
82-
rate float64
83-
init bool
82+
rate uint64
83+
init uint32
8484
mutex sync.Mutex
8585
}
8686

8787
// Rate returns the moving average rate of events per second.
8888
func (a *StandardEWMA) Rate() float64 {
89-
a.mutex.Lock()
90-
defer a.mutex.Unlock()
91-
return a.rate * float64(1e9)
89+
currentRate := math.Float64frombits(atomic.LoadUint64(&a.rate)) * float64(1e9)
90+
return currentRate
9291
}
9392

9493
// Snapshot returns a read-only copy of the EWMA.
@@ -99,17 +98,38 @@ func (a *StandardEWMA) Snapshot() EWMA {
9998
// Tick ticks the clock to update the moving average. It assumes it is called
10099
// every five seconds.
101100
func (a *StandardEWMA) Tick() {
101+
// Optimization to avoid mutex locking in the hot-path.
102+
if atomic.LoadUint32(&a.init) == 1 {
103+
a.updateRate(a.fetchInstantRate())
104+
} else {
105+
// Slow-path: this is only needed on the first Tick() and preserves transactional updating
106+
// of init and rate in the else block. The first conditional is needed below because
107+
// a different thread could have set a.init = 1 between the time of the first atomic load and when
108+
// the lock was acquired.
109+
a.mutex.Lock()
110+
if atomic.LoadUint32(&a.init) == 1 {
111+
// The fetchInstantRate() uses atomic loading, which is unecessary in this critical section
112+
// but again, this section is only invoked on the first successful Tick() operation.
113+
a.updateRate(a.fetchInstantRate())
114+
} else {
115+
atomic.StoreUint32(&a.init, 1)
116+
atomic.StoreUint64(&a.rate, math.Float64bits(a.fetchInstantRate()))
117+
}
118+
a.mutex.Unlock()
119+
}
120+
}
121+
122+
func (a *StandardEWMA) fetchInstantRate() float64 {
102123
count := atomic.LoadInt64(&a.uncounted)
103124
atomic.AddInt64(&a.uncounted, -count)
104125
instantRate := float64(count) / float64(5e9)
105-
a.mutex.Lock()
106-
defer a.mutex.Unlock()
107-
if a.init {
108-
a.rate += a.alpha * (instantRate - a.rate)
109-
} else {
110-
a.init = true
111-
a.rate = instantRate
112-
}
126+
return instantRate
127+
}
128+
129+
func (a *StandardEWMA) updateRate(instantRate float64) {
130+
currentRate := math.Float64frombits(atomic.LoadUint64(&a.rate))
131+
currentRate += a.alpha * (instantRate - currentRate)
132+
atomic.StoreUint64(&a.rate, math.Float64bits(currentRate))
113133
}
114134

115135
// Update adds n uncounted events.

ewma_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ func BenchmarkEWMA(b *testing.B) {
1111
}
1212
}
1313

14+
func BenchmarkEWMAParallel(b *testing.B) {
15+
a := NewEWMA1()
16+
b.ResetTimer()
17+
18+
b.RunParallel(func(pb *testing.PB) {
19+
for pb.Next() {
20+
a.Update(1)
21+
a.Tick()
22+
}
23+
})
24+
}
25+
1426
func TestEWMA1(t *testing.T) {
1527
a := NewEWMA1()
1628
a.Update(3)

0 commit comments

Comments
 (0)