資源鏈接
介紹
- 就是一個(gè)池子慰照,可以暫時(shí)存儲(chǔ)對(duì)象并查詢對(duì)象食绿。
- 任何存儲(chǔ)在池子里面的對(duì)象可能會(huì)被自動(dòng)移除(GC時(shí)),如果此時(shí)池子僅僅保留了引用,那么對(duì)象將會(huì)被 deallocated蠢甲。
- 一個(gè)池子可以被多個(gè)goroutines同時(shí)安全地訪問遂鹊。
引發(fā)的關(guān)于 Cache 和 Pool 的爭(zhēng)論
golang sync.Pool試用說明及注意事項(xiàng)
gc(garbage collector)
- Go 是自動(dòng)垃圾回收的肝集,減少了程序員的負(fù)擔(dān)
- GC 是一把雙刃劍抓谴,帶來便利但是也增加了開銷,使用不當(dāng)會(huì)嚴(yán)重影響程序的性能
- 高性能場(chǎng)景下怨愤,不能任意產(chǎn)生太多的垃圾(GC 負(fù)擔(dān)重派敷,會(huì)影響性能)
如何解決GC負(fù)擔(dān)重?
- 避免大家重復(fù)造輪子撰洗,開發(fā)了 Pool 包來保存和復(fù)用臨時(shí)對(duì)象篮愉,以減少內(nèi)存分配,降低 GC 壓力
-
http://echo.labstack.com/guide/routing
Echo 的路由使用了 sync pool 來重復(fù)利用內(nèi)存并且?guī)缀踹_(dá)到了零內(nèi)存占用 - gin 的 context 通過 pool 來 get 和 put差导,也就是使用了 sync.Pool 進(jìn)行維護(hù)
兩種使用方式
// 方法一
package main
import(
"fmt"
"log"
"runtime"
"sync"
)
func main(){
p := &sync.Pool{
New: func() interface{} {
return 0
},
}
a := p.Get().(int)
p.Put(1)
b := p.Get().(int)
fmt.Println(a, b) // 輸出 0 1
p.Put(3)
p.Put(4)
p.Put(5)
log.Println(p.Get()) // 返回 3 4 5 中的任意一個(gè)
// 主動(dòng)調(diào)用 GC试躏, pool 中的對(duì)象會(huì)被列入 victim 緩存
runtime.GC()
c := p.Get().(int)
log.Println(c) // 拿到的是 4
// 再次調(diào)用 GC, pool 中的 victim 緩存會(huì)被刪除
runtime.GC()
c = p.Get().(int)
log.Println(c) // 拿到的是 0
}
// 方法二
package main
import(
"fmt"
"sync"
)
func main(){
// 如果我們不指定 New 函數(shù)的話设褐,會(huì)返回 nil
p := &sync.Pool{}
a := p.Get()
if a == nil {
a = func() interface{} {
return 0
}
}
p.Put(1)
b := p.Get().(int)
fmt.Println(a, b) // 輸出 0 1
}
Pool 源碼解讀
Pool 結(jié)構(gòu)
type Pool struct {
// 用來標(biāo)記颠蕴,當(dāng)前的 struct 是不能夠被 copy 的
noCopy noCopy
// P 個(gè)固定大小的 poolLocal 數(shù)組,每個(gè) P 擁有一個(gè)空間
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
// 上面數(shù)組的大小络断,即 P 的個(gè)數(shù)
localSize uintptr // size of the local array
// 同 local 和 localSize裁替,只是在 gc 的過程中保留一次
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// 自定義一個(gè) New 函數(shù),然后可以在 Get 不到東西時(shí)貌笨,自動(dòng)創(chuàng)建一個(gè)
New func() interface{}
}
因?yàn)?Pool 不希望被復(fù)制,所以結(jié)構(gòu)體里有一個(gè) noCopy 的字段襟沮,使用 go vet 工具可以檢測(cè)到用戶代碼是否復(fù)制了 Pool锥惋。noCopy
是 go1.7 開始引入的一個(gè)靜態(tài)檢查機(jī)制昌腰。它不僅僅工作在運(yùn)行時(shí)或標(biāo)準(zhǔn)庫,同時(shí)也對(duì)用戶代碼有效膀跌。用戶只需實(shí)現(xiàn)這樣的不消耗內(nèi)存遭商、僅用于靜態(tài)分析的結(jié)構(gòu),來保證一個(gè)對(duì)象在第一次使用后不會(huì)發(fā)生復(fù)制捅伤。
// Local per-P Pool appendix.
type poolLocalInternal struct {
// private 存儲(chǔ)一個(gè) Put 的數(shù)據(jù)劫流,pool.Put() 操作優(yōu)先存入 private,如果private有信息丛忆,才會(huì)存入 shared
private interface{} // Can be used only by the respective P.
// 存儲(chǔ)一個(gè)鏈表祠汇,用來維護(hù) pool.Put() 操作加入的數(shù)據(jù),每個(gè) P 可以操作自己 shared 鏈表中的頭部熄诡,而其他的 P 在用完自己的 shared 時(shí)可很,可能會(huì)來偷數(shù)據(jù),從而操作鏈表的尾部
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
// unsafe.Sizeof(poolLocal{}) // 128 byte(1byte = 8 bits)
// unsafe.Sizeof(poolLocalInternal{}) // 32 byte(1byte = 8 bits)
type poolLocal struct {
poolLocalInternal
// Prevents false sharing on widespread platforms with
// 128 mod (cache line size) = 0 .
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
Get()
func (p *Pool) Get() interface{} {
...
l, pid := p.pin() // 獲取當(dāng)前 pool 的 poolLocal凰浮,也就是 p.local[pid]
x := l.private // 判斷當(dāng)前的臨時(shí)變量是否有值我抠,有則立即返回
l.private = nil
if x == nil {
// Try to pop the head of the local shard. We prefer
// the head over the tail for temporal locality of
// reuse.
x, _ = l.shared.popHead() // 從 shared poolChain 鏈表里面獲取頭部數(shù)據(jù)
if x == nil {
x = p.getSlow(pid) // 本線程的 Pool 沒有數(shù)據(jù)了,就去其他線程的 Pool 池取
}
}
...
// 無法獲取到值袜茧,則 New 一個(gè)菜拓,未設(shè)定 New 函數(shù)則返回 nil
if x == nil && p.New != nil {
x = p.New()
}
return x
}
Put()
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
...
l, _ := p.pin() // 獲取當(dāng)前 pool 的 poolLocal,也就是 p.local[pid]笛厦,這里不關(guān)心 pid
// 優(yōu)先寫入 private 變量
if l.private == nil {
l.private = x
x = nil
}
// 如果 private 有值纳鼎,則寫入 shared poolChain 鏈表
if x != nil {
l.shared.pushHead(x)
}
...
}
indexLocal()
獲取線程 pid (i) 對(duì)應(yīng)的 poolLocal,因?yàn)槭莻€(gè)數(shù)組递递,即 0+offset
func indexLocal(l unsafe.Pointer, i int) *poolLocal {
lp := unsafe.Pointer(uintptr(l) + uintptr(i)*unsafe.Sizeof(poolLocal{}))
return (*poolLocal)(lp)
}
getSlow()
func (p *Pool) getSlow(pid int) interface{} {
// See the comment in pin regarding ordering of the loads.
size := atomic.LoadUintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// Try to steal one element from other procs. 從其他的線程偷數(shù)據(jù)
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size)) // 從當(dāng)前 pid 的 local 開始遍歷其他線程的 pool 池(poolLocal)喷橙,遍歷一個(gè)圈。返回值為其他線程的 pool.poolLocal
// 從尾部獲取數(shù)據(jù)
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 當(dāng)無法從其他線程的 poolLocal 得到信息登舞,則從 victim 緩存區(qū)域獲确∮狻(和 local 一樣的邏輯)
size = atomic.LoadUintptr(&p.victimSize)
if uintptr(pid) >= size {
return nil
}
locals = p.victim
l := indexLocal(locals, pid)
if x := l.private; x != nil {
l.private = nil
return x
}
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
// 如果 victim 全空,則 victimSize 設(shè)置為 0菠秒,防止下次再次遍歷
atomic.StoreUintptr(&p.victimSize, 0)
return nil
}
pin()
pin 函數(shù) pins 當(dāng)前 goroutine 的 P疙剑,防止 preemption
returns poolLocal pool for the P and the P's id.
調(diào)用方當(dāng)完成對(duì) pool 的操作后,必須調(diào)用 runtime_procUnpin()
func (p *Pool) pin() (*poolLocal, int) {
pid := runtime_procPin()
// In pinSlow we store to local and then to localSize, here we load in opposite order.
// Since we've disabled preemption, GC cannot happen in between.
// Thus here we must observe local at least as large localSize.
// We can observe a newer/larger local, it is fine (we must observe its zero-initialized-ness).
s := atomic.LoadUintptr(&p.localSize) // load-acquire
l := p.local // load-consume
if uintptr(pid) < s {
return indexLocal(l, pid), pid // 獲取當(dāng)前線程的 poolLocal 和 pid
}
return p.pinSlow()
}
// 該函數(shù)鏈接于 runtime.proc.go:sync_runtime_procPin 函數(shù)
func runtime_procPin(){}
//go:linkname sync_runtime_procPin sync.runtime_procPin
//go:nosplit
func sync_runtime_procPin() int {
return procPin()
}
//go:nosplit
func procPin() int {
_g_ := getg()
mp := _g_.m
mp.locks++
return int(mp.p.ptr().id)
}
//go:nosplit
func procUnpin() {
_g_ := getg()
_g_.m.locks--
}
pinSlow()
func (p *Pool) pinSlow() (*poolLocal, int) {
// 由于調(diào)用該函數(shù)前 pin 過践叠,這里需要 unpin言缤,否則 allPoolsMu 無法被加鎖
runtime_procUnpin()
// 對(duì) allPools 變量加鎖,來操作 allPools禁灼,這里存儲(chǔ)所有的 pool
allPoolsMu.Lock()
defer allPoolsMu.Unlock()
// 重新 pin 當(dāng)前線程的 P
pid := runtime_procPin()
// pin 后 poolCleanup 不會(huì)被調(diào)用
s := p.localSize
l := p.local
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
if p.local == nil {
allPools = append(allPools, p)
}
// 如果 GOMAXPROCS 在 GCs 時(shí)發(fā)生了改變管挟,我們重新分配 local,并設(shè)置 localSize
size := runtime.GOMAXPROCS(0) // 獲取線程數(shù)
local := make([]poolLocal, size) // 每個(gè)線程一個(gè) poolLocal弄捕,所以這里設(shè)置為 size 個(gè)大小的數(shù)組
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
atomic.StoreUintptr(&p.localSize, uintptr(size)) // store-release
return &local[pid], pid
}
poolCleanUp()
該函數(shù)在 init 函數(shù)中注冊(cè)到 runtime 中僻孝,在調(diào)用 GC 前导帝,函數(shù)被調(diào)用
func poolCleanup() {
// Drop victim caches from all pools.
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Move primary cache to victim cache.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
// 所有的池都丟掉主緩存,并數(shù)據(jù)移動(dòng)到 victim 緩存
oldPools, allPools = allPools, nil
}
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
// 該函數(shù)鏈接于 runtime.mgc.go:sync_runtime_registerPoolCleanup
func runtime_registerPoolCleanup(cleanup func())
//go:linkname sync_runtime_registerPoolCleanup sync.runtime_registerPoolCleanup
func sync_runtime_registerPoolCleanup(f func()) {
poolcleanup = f
}
總結(jié)
sync.Pool 的特性
- 池不能夠指定大小穿铆,大小只受限于 GC 的臨界值(GOMAXPROCS)
- 對(duì)象最大的緩存周期是兩個(gè) GC 周期您单,每次 GC ,當(dāng)前的 primary cache 會(huì)被轉(zhuǎn)移到 victim cache荞雏,primary cache 清空虐秦,而原來 victim cache 被釋放
- 取值順序:當(dāng)前 P 的 primary cache(local)的 poolLocal.private → 當(dāng)前 P 的 primary cache(local)的 poolLocal.shared.head → 其他 P 的主存(local)的 poolLocal.shared.tail → 當(dāng)前 P 的 victim cache(victim) 的 poolLocal.private → 當(dāng)前 P 的 victim cache(victim) 的 poolLocal.shared.head → 其他 P 的主存(local)的 poolLocal.shared.tail → p.New() → nil
- 插入順序:當(dāng)前 P 的 primary cache(local)的 poolLocal.private → 當(dāng)前 P 的 primary cache(local)的 poolLocal.shared.head
Use sync.Pool
- How to implement Memory Pooling in Golang
- How did I improve latency by 700% using sync.Pool
- 【譯】CockroachDB GC優(yōu)化總結(jié)
- Golang 優(yōu)化之路
以下是視頻
- Mastering Go Programming: Syncs and Locks | packtpub,com
-
justforfunc #37: sync.Pool from the pool
優(yōu)化的情況 - SREcon17 Asia/Australia: Golang's Garbage
- Google Go 團(tuán)隊(duì)如何進(jìn)行 CodeReview