Skip to content

Commit 2dcbf8b

Browse files
committed
sync: smooth out Pool behavior over GC with a victim cache
Currently, every Pool is cleared completely at the start of each GC. This is a problem for heavy users of Pool because it causes an allocation spike immediately after Pools are clear, which impacts both throughput and latency. This CL fixes this by introducing a victim cache mechanism. Instead of clearing Pools, the victim cache is dropped and the primary cache is moved to the victim cache. As a result, in steady-state, there are (roughly) no new allocations, but if Pool usage drops, objects will still be collected within two GCs (as opposed to one). This victim cache approach also improves Pool's impact on GC dynamics. The current approach causes all objects in Pools to be short lived. However, if an application is in steady state and is just going to repopulate its Pools, then these objects impact the live heap size *as if* they were long lived. Since Pooled objects count as short lived when computing the GC trigger and goal, but act as long lived objects in the live heap, this causes GC to trigger too frequently. If Pooled objects are a non-trivial portion of an application's heap, this increases the CPU overhead of GC. The victim cache lets Pooled objects affect the GC trigger and goal as long-lived objects. This has no impact on Get/Put performance, but substantially reduces the impact to the Pool user when a GC happens. PoolExpensiveNew demonstrates this in the substantially reduction in the rate at which the "New" function is called. name old time/op new time/op delta Pool-12 2.21ns ±36% 2.00ns ± 0% ~ (p=0.070 n=19+16) PoolOverflow-12 587ns ± 1% 583ns ± 1% -0.77% (p=0.000 n=18+18) PoolSTW-12 5.57µs ± 3% 4.52µs ± 4% -18.82% (p=0.000 n=20+19) PoolExpensiveNew-12 3.69ms ± 7% 1.25ms ± 5% -66.25% (p=0.000 n=20+19) name old p50-ns/STW new p50-ns/STW delta PoolSTW-12 5.48k ± 2% 4.53k ± 2% -17.32% (p=0.000 n=20+20) name old p95-ns/STW new p95-ns/STW delta PoolSTW-12 6.69k ± 4% 5.13k ± 3% -23.31% (p=0.000 n=19+18) name old GCs/op new GCs/op delta PoolExpensiveNew-12 0.39 ± 1% 0.32 ± 2% -17.95% (p=0.000 n=18+20) name old New/op new New/op delta PoolExpensiveNew-12 40.0 ± 6% 12.4 ± 6% -68.91% (p=0.000 n=20+19) (https://perf.golang.org/search?q=upload:20190311.2) Fixes #22950. Change-Id: If2e183d948c650417283076aacc20739682cdd70 Reviewed-on: https://go-review.googlesource.com/c/go/+/166961 Run-TryBot: Austin Clements <austin@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: David Chase <drchase@google.com>
1 parent d5fd2dd commit 2dcbf8b

File tree

2 files changed

+66
-9
lines changed

2 files changed

+66
-9
lines changed

src/sync/pool.go

+54-6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ type Pool struct {
4747
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
4848
localSize uintptr // size of the local array
4949

50+
victim unsafe.Pointer // local from previous cycle
51+
victimSize uintptr // size of victims array
52+
5053
// New optionally specifies a function to generate
5154
// a value when Get would otherwise return nil.
5255
// It may not be changed concurrently with calls to Get.
@@ -150,14 +153,39 @@ func (p *Pool) Get() interface{} {
150153
func (p *Pool) getSlow(pid int) interface{} {
151154
// See the comment in pin regarding ordering of the loads.
152155
size := atomic.LoadUintptr(&p.localSize) // load-acquire
153-
local := p.local // load-consume
156+
locals := p.local // load-consume
154157
// Try to steal one element from other procs.
155158
for i := 0; i < int(size); i++ {
156-
l := indexLocal(local, (pid+i+1)%int(size))
159+
l := indexLocal(locals, (pid+i+1)%int(size))
160+
if x, _ := l.shared.popTail(); x != nil {
161+
return x
162+
}
163+
}
164+
165+
// Try the victim cache. We do this after attempting to steal
166+
// from all primary caches because we want objects in the
167+
// victim cache to age out if at all possible.
168+
size = atomic.LoadUintptr(&p.victimSize)
169+
if uintptr(pid) >= size {
170+
return nil
171+
}
172+
locals = p.victim
173+
l := indexLocal(locals, pid)
174+
if x := l.private; x != nil {
175+
l.private = nil
176+
return x
177+
}
178+
for i := 0; i < int(size); i++ {
179+
l := indexLocal(locals, (pid+i)%int(size))
157180
if x, _ := l.shared.popTail(); x != nil {
158181
return x
159182
}
160183
}
184+
185+
// Mark the victim cache as empty for future gets don't bother
186+
// with it.
187+
atomic.StoreUintptr(&p.victimSize, 0)
188+
161189
return nil
162190
}
163191

@@ -208,17 +236,37 @@ func poolCleanup() {
208236

209237
// Because the world is stopped, no pool user can be in a
210238
// pinned section (in effect, this has all Ps pinned).
211-
for i, p := range allPools {
212-
allPools[i] = nil
239+
240+
// Drop victim caches from all pools.
241+
for _, p := range oldPools {
242+
p.victim = nil
243+
p.victimSize = 0
244+
}
245+
246+
// Move primary cache to victim cache.
247+
for _, p := range allPools {
248+
p.victim = p.local
249+
p.victimSize = p.localSize
213250
p.local = nil
214251
p.localSize = 0
215252
}
216-
allPools = []*Pool{}
253+
254+
// The pools with non-empty primary caches now have non-empty
255+
// victim caches and no pools have primary caches.
256+
oldPools, allPools = allPools, nil
217257
}
218258

219259
var (
220260
allPoolsMu Mutex
221-
allPools []*Pool
261+
262+
// allPools is the set of pools that have non-empty primary
263+
// caches. Protected by either 1) allPoolsMu and pinning or 2)
264+
// STW.
265+
allPools []*Pool
266+
267+
// oldPools is the set of pools that may have non-empty victim
268+
// caches. Protected by STW.
269+
oldPools []*Pool
222270
)
223271

224272
func init() {

src/sync/pool_test.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,20 @@ func TestPool(t *testing.T) {
4141
}
4242
Runtime_procUnpin()
4343

44-
p.Put("c")
45-
debug.SetGCPercent(100) // to allow following GC to actually run
44+
// Put in a large number of objects so they spill into
45+
// stealable space.
46+
for i := 0; i < 100; i++ {
47+
p.Put("c")
48+
}
49+
// After one GC, the victim cache should keep them alive.
50+
runtime.GC()
51+
if g := p.Get(); g != "c" {
52+
t.Fatalf("got %#v; want c after GC", g)
53+
}
54+
// A second GC should drop the victim cache.
4655
runtime.GC()
4756
if g := p.Get(); g != nil {
48-
t.Fatalf("got %#v; want nil after GC", g)
57+
t.Fatalf("got %#v; want nil after second GC", g)
4958
}
5059
}
5160

0 commit comments

Comments
 (0)