精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

常見(jiàn)golang gc的內(nèi)部?jī)?yōu)化方案

馬哥Linux運(yùn)維 ? 來(lái)源:cnblogs ? 2024-03-29 11:19 ? 次閱讀

今天講一個(gè)常見(jiàn)的gc compiler(也就是官方版本的go編譯器和runtime)在垃圾回收的掃描標(biāo)記階段做的優(yōu)化。

我對(duì)這個(gè)優(yōu)化的描述印象最深的是在bigcache的注釋里,大致內(nèi)容是如果map的鍵值都不包含指針,那么gc掃描的時(shí)候不管這個(gè)map多大都不會(huì)深入掃描map內(nèi)部存儲(chǔ)的數(shù)據(jù),只檢查map本身是否需要回收。

這么做的好處顯然是可以讓gc的掃描速度大大增加,從而減少gc對(duì)性能的損耗。

減少指針數(shù)量本身就是常見(jiàn)的優(yōu)化手段,但讓我感到好奇的是注釋里說(shuō)的“跳過(guò)”。跳過(guò)的依據(jù)究竟是什么,以及只有map存在這種跳過(guò)嗎?

于是我進(jìn)行了全面的搜索,結(jié)果除了復(fù)讀bigcache里那段話的,沒(méi)什么有用的發(fā)現(xiàn)。

于是這篇文章誕生了。

跳過(guò)掃描指的是什么

前置知識(shí)少不得。

簡(jiǎn)單的說(shuō),gc在檢查對(duì)象是否存活的時(shí)候,除了對(duì)象本身,還要檢查對(duì)象的子對(duì)象是否引用了其他對(duì)象,具體來(lái)說(shuō):

數(shù)組和slice的話指存儲(chǔ)在里面的每一個(gè)元素是否存活,這里被存儲(chǔ)的元素是數(shù)組/slice的子對(duì)象

map的子對(duì)象就是里面存的鍵和值了

struct的子對(duì)象是它的每一個(gè)字段

為了檢查這些子對(duì)象是否引用了其他對(duì)象(關(guān)系到這些被引用的對(duì)象是否能被回收),gc需要深入掃描這些子對(duì)象。子對(duì)象越多需要掃描的東西就越多。而且這個(gè)過(guò)程是遞歸的,因?yàn)樽訉?duì)象也會(huì)有子對(duì)象,想象一下嵌套的數(shù)組或者map。

跳過(guò)掃描自然就是指跳過(guò)這些子對(duì)象的掃描,只需要檢查對(duì)象本身即可的操作。

什么樣的對(duì)象是可以跳過(guò)掃描的

這也是我的第一個(gè)疑問(wèn)。跳過(guò)或不跳過(guò)的依據(jù)是什么,又或者是什么東西在控制這一過(guò)程。

bigcache告訴我們存有不包含指針的鍵值對(duì)的map是可以跳過(guò)的,那么具體情況是怎么樣的呢?

找不到有用的資料,那只能看代碼了,代碼以Go 1.22.1為準(zhǔn)。

首先應(yīng)該想到的應(yīng)該是從gc的代碼開(kāi)始看,于是很快就有了收獲:

// runtime/mgcmark.go
// 負(fù)責(zé)gc掃描的函數(shù),還有個(gè)它的兄弟gcDrainN,代碼差不多就不放了
func gcDrain(gcw *gcWork, flags gcDrainFlags) {
...
// 先標(biāo)記所有root對(duì)象,檢查對(duì)象是否存活就是從這開(kāi)始的
if work.markrootNext < work.markrootJobs {
for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0)) {
markroot(gcw, job, flushBgCredit)
// 檢查自己是否需要被中斷,需要的場(chǎng)合函數(shù)會(huì)直接跳到收尾工作然后返回
}
}
// 從工作隊(duì)列里拿需要掃描的對(duì)象進(jìn)行處理
for !(gp.preempt && (preemptible || sched.gcwaiting.Load() || pp.runSafePointFn != 0)) {
b := gcw.tryGetFast() // 從工作隊(duì)列拿對(duì)象
scanobject(b, gcw)
...
}
...
}

流程不考慮中斷、數(shù)據(jù)統(tǒng)計(jì)和校驗(yàn)的話還是很簡(jiǎn)單的,就是先標(biāo)記掃描的起點(diǎn),然后從gcw這個(gè)工作隊(duì)列里拿東西出來(lái)處理,直到工作隊(duì)列里再也沒(méi)數(shù)據(jù)了為止。

markroot也很簡(jiǎn)單,根據(jù)root對(duì)象的種類(lèi),它會(huì)調(diào)用scanblock或者markrootSpans。其中scanblock會(huì)調(diào)用greyobject來(lái)標(biāo)記待處理的對(duì)象。因此稍微看看markrootSpans即可。

markrootSpans是用來(lái)處理那些存放設(shè)置了終結(jié)器的對(duì)象的內(nèi)存的:

// runtime/mgcmark.go
func markrootSpans(gcw *gcWork, shard int) {
...
for i := range specialsbits {
...
for j := uint(0); j < 8; j++ {
// 找到要處理的span(go內(nèi)存使用的單位,你就當(dāng)是“一塊內(nèi)存空間”就行)
s := ha.spans[arenaPage+uint(i)*8+j]
...
lock(&s.speciallock)
for sp := s.specials; sp != nil; sp = sp.next {
if sp.kind != _KindSpecialFinalizer {
continue
}
// don't mark finalized object, but scan it so we
// retain everything it points to.
// spf是終結(jié)器本身
spf := (*specialfinalizer)(unsafe.Pointer(sp))
// A finalizer can be set for an inner byte of an object, find object beginning.
p := s.base() + uintptr(spf.special.offset)/s.elemsize*s.elemsize
// p是設(shè)置了終結(jié)器的對(duì)象
// 這里檢查這個(gè)對(duì)象占用的內(nèi)存上是否設(shè)置了跳過(guò)掃描的標(biāo)記
// 設(shè)置了的話就不要繼續(xù)掃描對(duì)象自己的子對(duì)象了
if !s.spanclass.noscan() {
scanobject(p, gcw)
}
// 這個(gè)span本身就是root對(duì)象,所以剩下的直接用scanblock處理
scanblock(uintptr(unsafe.Pointer(&spf.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
}
unlock(&s.speciallock)
}
}
}

其實(shí)很簡(jiǎn)單,依舊是找到所有的對(duì)象,然后進(jìn)行處理。然而我們看到了有意思的東西:s.spanclass.noscan()。

看起來(lái)這和是否跳過(guò)掃描有關(guān)。

但我們先不深入這個(gè)方法,為什么?因?yàn)榻K結(jié)器是被特殊處理的,沒(méi)看完scanobject和greyobject之前我們不能斷言這個(gè)方法是否控制著對(duì)對(duì)象的掃描。(其實(shí)注釋上我已經(jīng)告訴你就是這個(gè)東西控制的了,但如果你自己跟蹤代碼的話頭一次看到這段代碼的時(shí)候是不知道的)

所以我們接著看scanobject,這個(gè)函數(shù)是掃描對(duì)象的子對(duì)象的:

// runtime/mgcmark.go
func scanobject(b uintptr, gcw *gcWork) {
// 先拿到還沒(méi)掃描過(guò)的內(nèi)存
s := spanOfUnchecked(b)
n := s.elemsize
// n 表示mspan里有幾個(gè)對(duì)象,在被這個(gè)函數(shù)檢查的時(shí)候肯定不能是0
if n == 0 {
throw("scanobject n == 0")
}
if s.spanclass.noscan() {
// 如果內(nèi)存設(shè)置了noscan標(biāo)志,就報(bào)錯(cuò)
throw("scanobject of a noscan object")
}
var tp typePointers
if n > maxObletBytes {
// 大內(nèi)存分割成不同的塊放進(jìn)工作隊(duì)列,這樣能被并行處理
if b == s.base() {
// 分割后入隊(duì)
for oblet := b + maxObletBytes; oblet < s.base()+s.elemsize; oblet += maxObletBytes {
if !gcw.putFast(oblet) {
gcw.put(oblet)
}
}
}
// 獲取類(lèi)型信息
} else {
// 這里不重要
}
var scanSize uintptr
for {
var addr uintptr
// 獲取子對(duì)象
// 整個(gè)循環(huán)的退出條件就是next不再返回子對(duì)象的時(shí)候(沒(méi)東西可繼續(xù)掃描了)
if tp, addr = tp.nextFast(); addr == 0 {
if tp, addr = tp.next(); addr == 0 {
break
}
}
// 拿到要處理的對(duì)象
scanSize = addr - b + goarch.PtrSize
obj := *(*uintptr)(unsafe.Pointer(addr))
// 排除nil和指向當(dāng)前對(duì)象自身的指針
// 后者屬于可以被回收的循環(huán)引用,當(dāng)前對(duì)象能不能回收不受這個(gè)指針影響
// 因?yàn)槿绻?dāng)前對(duì)象不可訪問(wèn)了,那么它的字段自然也是不可能被訪問(wèn)到的,兩者均從root不可達(dá)
// 而如果這個(gè)指針是可達(dá)的,那么當(dāng)前對(duì)象的字段被引用,當(dāng)前對(duì)象也是不需要回收的
// 所以指向當(dāng)前對(duì)象本身的指針字段不需要處理
if obj != 0 && obj-b >= n {
if obj, span, objIndex := findObject(obj, b, addr-b); obj != 0 {
greyobject(obj, b, addr-b, span, gcw, objIndex)
}
}
}
...
}

這個(gè)函數(shù)長(zhǎng)歸長(zhǎng),條理還是清晰的:

首先看看對(duì)象是否太大要把對(duì)象的內(nèi)存分割成小塊交給工作隊(duì)列里的其他協(xié)程并行處理

接著掃描所有子對(duì)象,用greyobject標(biāo)記這些對(duì)象

因?yàn)檫@個(gè)函數(shù)本身已經(jīng)是在掃描了,所以不太會(huì)有“跳過(guò)”的相關(guān)的邏輯,而且你也看到了把這個(gè)函數(shù)放在不需要掃描子對(duì)象的對(duì)象上調(diào)用時(shí)會(huì)觸發(fā)throw,throw會(huì)導(dǎo)致程序報(bào)錯(cuò)并退出執(zhí)行。

所以秘密就在greyobject里了。看看代碼:

// runtime/mgcmark.go
func greyobject(obj, base, off uintptr, span *mspan, gcw *gcWork, objIndex uintptr) {
...
if useCheckmark {
if setCheckmark(obj, base, off, mbits) {
// Already marked.
return
}
} else {
...
// If marked we have nothing to do.
if mbits.isMarked() {
return
}
mbits.setMarked()
...
// 如果內(nèi)存被標(biāo)記為不需要進(jìn)一步掃描,則會(huì)跳過(guò)后續(xù)的流程(內(nèi)存會(huì)被放進(jìn)gc掃描的工作隊(duì)列里等著被取出來(lái)掃描)
if span.spanclass.noscan() {
...
return
}
}
// 對(duì)象被放進(jìn)工作隊(duì)列等待掃描
}

這個(gè)函數(shù)會(huì)先檢查對(duì)象是否已經(jīng)被處理過(guò),然后標(biāo)記對(duì)象,接著檢查span上的noscan標(biāo)志,設(shè)置了的話就返回調(diào)用,沒(méi)有設(shè)置說(shuō)明需要被進(jìn)一步掃描,于是被放進(jìn)工作隊(duì)列,等著gcDrain或者它兄弟來(lái)處理。

現(xiàn)在我們可以得出結(jié)論了,會(huì)不會(huì)跳過(guò)掃描,全部由內(nèi)存上是否設(shè)置noscan標(biāo)志來(lái)控制,設(shè)置了就可以跳過(guò)。

至于在這塊內(nèi)存上的是map還是slice還是struct,沒(méi)關(guān)系。

跳過(guò)掃描的具體流程

看了上面的代碼,我想信你一定是懵的,跳過(guò)具體發(fā)生的流程是什么樣的呢?

沒(méi)關(guān)系,我們看兩個(gè)例子就知道了。

第一個(gè)例子是一個(gè)頂層的全局的可跳過(guò)掃描的對(duì)象A,介于我們還沒(méi)說(shuō)noscan會(huì)在什么情況下被設(shè)置,所以我們先忽略A的具體類(lèi)型,只要知道它可以跳過(guò)掃描即可。

A的掃描流程是這樣的:

gc開(kāi)始運(yùn)行,先標(biāo)記root對(duì)象

A就是root之一,所以它要么被scanblock處理要么被markrootSpan處理

假設(shè)A設(shè)置了終結(jié)器,又因?yàn)锳是可跳過(guò)掃描子對(duì)象的,因此markrootSpan會(huì)直接調(diào)用scanblock

scanblock會(huì)調(diào)用greyobject處理內(nèi)存里的對(duì)象

因?yàn)锳可跳過(guò)掃描,所以greyobject做完標(biāo)記就返回了,A不會(huì)進(jìn)入工作隊(duì)列

A的掃描結(jié)束,整個(gè)流程上不會(huì)有scanobject的調(diào)用

A的例子相對(duì)簡(jiǎn)單,現(xiàn)在我們假設(shè)有個(gè)不是root對(duì)象的對(duì)象B,B本身不可跳過(guò)掃描,B有一個(gè)子對(duì)象C可以跳過(guò)掃描。我們來(lái)看看C的掃描流程:

因?yàn)锽并不是root對(duì)象,且不可跳過(guò)掃描,所以它作為某個(gè)root對(duì)象的子對(duì)象,現(xiàn)在肯定在gc工作隊(duì)列里

gcDrain從隊(duì)列里拿到了B,于是交給了scanobject處理

我們假設(shè)B不是很大因此不會(huì)被分割(反正分割了也一樣)

scanobject把每個(gè)B的子對(duì)象都用greyobject處理,C也不例外

因?yàn)镃可跳過(guò)掃描,所以greyobject做完標(biāo)記就返回了,C不會(huì)進(jìn)入工作隊(duì)列

C的掃描結(jié)束,整個(gè)流程上不會(huì)有對(duì)C的scanobject的調(diào)用

這樣基本涵蓋了所有的情況,一些我沒(méi)單獨(dú)說(shuō)的比如“可跳過(guò)對(duì)象E是不可跳過(guò)root對(duì)象D的子對(duì)象”這樣的情況,實(shí)際上和情況2沒(méi)什么區(qū)別。

現(xiàn)在對(duì)象的子對(duì)象掃描是這么跳過(guò)的我們也知道了,只剩一個(gè)疑問(wèn)了:noscan標(biāo)志是怎么設(shè)置的?

noscan標(biāo)志是怎么設(shè)置的

在深入之前,我們先來(lái)簡(jiǎn)單看下go的怎么分配內(nèi)存的。完整講解恐怕5篇長(zhǎng)文也兜不住,所以我做些概念上的精簡(jiǎn)。

在go里,mspan是內(nèi)存分配的基礎(chǔ)單位,一個(gè)mspan上可以分配多個(gè)大小類(lèi)似可以被歸為一類(lèi)的對(duì)象(比如13字節(jié)和14字節(jié)的對(duì)象都是一類(lèi),可以被分配到允許最大存儲(chǔ)16字節(jié)對(duì)象的mspan上)。這個(gè)“類(lèi)型”就叫mpan的sizeclass。一個(gè)簡(jiǎn)單的心智模型是把mspan當(dāng)成一個(gè)能存大小相近的對(duì)象的列表。

為了加快內(nèi)存分配,go會(huì)給每個(gè)線程預(yù)分配一塊內(nèi)存,然后按sizeclass分成多份,每份對(duì)應(yīng)一個(gè)sizeclass的mspan。這個(gè)結(jié)構(gòu)叫mcache。

當(dāng)然了,總有對(duì)象的大小會(huì)超過(guò)所有mcache的sizeclass規(guī)定的范圍,這個(gè)時(shí)候go就會(huì)像系統(tǒng)申請(qǐng)一大塊內(nèi)存,然后把內(nèi)存交給mspan。

存儲(chǔ)了span信息的比如sizeclass和noscan的結(jié)構(gòu)叫spanClass。這個(gè)結(jié)構(gòu)會(huì)作為字段存儲(chǔ)在mspan的控制結(jié)構(gòu)里。

知道了這些之后,我們就能看懂s.spanclass.noscan()了,它的意思就是檢查mspan的spanclass信息是否設(shè)置了不需要掃描子對(duì)象的標(biāo)志。

而創(chuàng)建spanclass只能用makeSpanClass這個(gè)函數(shù):

// runtime/mheap.go
type spanClass uint8
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}

現(xiàn)在問(wèn)題簡(jiǎn)單了,我們只要追蹤誰(shuí)調(diào)用了這個(gè)函數(shù)就行,以及我們還知道額外的信息:這些調(diào)用者還需要從mcache或者系統(tǒng)申請(qǐng)內(nèi)存獲得mspan結(jié)構(gòu)。這樣一下范圍就收縮了。

按上面的思路,我們很快就找到了go分配內(nèi)存給對(duì)象的入口之一mallocgc:

// runtime/malloc.go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
// size是指類(lèi)型的大小
// typ是需要?jiǎng)?chuàng)建的對(duì)象的類(lèi)型信息,如果只是分配內(nèi)存,typ要傳nil
// typ是否是空的或者typ是否包含有指針
noscan := typ == nil || !typ.Pointers()
if 如果size足夠小可以從mspan上分配 {
if size滿(mǎn)足要求可以用tinyallocator分配 {
} else {
// 計(jì)算sizeclass(size對(duì)應(yīng)到哪一類(lèi)span)
spc := makeSpanClass(sizeclass, noscan) // noscan是這里傳進(jìn)去的
span = c.alloc[spc] // 從mcache拿mspan
v := nextFreeFast(span) // 從mspan真正拿到可用的內(nèi)存
// 后面是把內(nèi)存內(nèi)容清零和維護(hù)gc信息等代碼
}
} else {
// 大對(duì)象分配
// mcache.allocLarge也調(diào)用makeSpanClass(0, noscan),然后用mheap.alloc根據(jù)span的信息從系統(tǒng)申請(qǐng)內(nèi)存
span = c.allocLarge(size, noscan) // noscan是這里傳進(jìn)去的
// 后面是把內(nèi)存內(nèi)容清零和維護(hù)gc信息等代碼
}
}

即使sizeclass是一樣的,因?yàn)閚oscan的值不一樣,兩個(gè)spanClass的值也是不一樣的。對(duì)于可跳過(guò)掃描的大對(duì)象來(lái)說(shuō),會(huì)把為這個(gè)對(duì)象分配的內(nèi)存標(biāo)記為noscan;對(duì)于可跳過(guò)的小對(duì)象來(lái)說(shuō),會(huì)直接把這個(gè)小對(duì)象放在mcache提前分配的不需要深入掃描的內(nèi)存區(qū)域上。

那么這個(gè)mallocgc又是誰(shuí)調(diào)用的?答案太多了,因?yàn)閚ew,make都會(huì)用到它。我們用slice和map做例子看看。

首先是slice。這個(gè)非常簡(jiǎn)單,創(chuàng)建slice的入口是makeslice:

// runtime/slice.go
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}

slice中的元素的類(lèi)型信息被傳給了mallocgc。如果slice的元素不包含指針,那么slice是可以跳過(guò)掃描的。

map比較特殊,跳過(guò)掃描的是它的bucket,而bucket外界是看不到的:

// runtime/map.go
// 調(diào)用鏈:makemap -> makeBucketArray -> newarray -> mallocgc
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b)
nbuckets := base
// For small b, overflow buckets are unlikely.
// Avoid the overhead of the calculation.
if b >= 4 {
// Add on the estimated number of overflow buckets
// required to insert the median number of elements
// used with this value of b.
nbuckets += bucketShift(b - 4)
sz := t.Bucket.Size_ * nbuckets
up := roundupsize(sz, !t.Bucket.Pointers())
if up != sz {
nbuckets = up / t.Bucket.Size_
}
}
if dirtyalloc == nil {
// t.Bucket.Pointers() 返回鍵值對(duì)中是否包含指針
buckets = newarray(t.Bucket, int(nbuckets))
} else {
// dirtyalloc was previously generated by
// the above newarray(t.Bucket, int(nbuckets))
// but may not be empty.
buckets = dirtyalloc
size := t.Bucket.Size_ * nbuckets
if t.Bucket.Pointers() {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
if base != nbuckets {
// We preallocated some overflow buckets.
// To keep the overhead of tracking these overflow buckets to a minimum,
// we use the convention that if a preallocated overflow bucket's overflow
// pointer is nil, then there are more available by bumping the pointer.
// We need a safe non-nil pointer for the last overflow bucket; just use buckets.
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.BucketSize)))
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.BucketSize)))
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}
func newarray(typ *_type, n int) unsafe.Pointer {
if n == 1 {
return mallocgc(typ.Size_, typ, true)
}
mem, overflow := math.MulUintptr(typ.Size_, uintptr(n))
if overflow || mem > maxAlloc || n < 0 {
panic(plainError("runtime: allocation size out of range"))
}
return mallocgc(mem, typ, true)
}

可以看到要是鍵值對(duì)里都不包含指針的話,map就可以被跳過(guò)。

所以總結(jié)下,只要?jiǎng)?chuàng)建的對(duì)象不包含指針(例如數(shù)組/切片成員都是不包含指針的類(lèi)型,map的鍵值對(duì)都不包含指針,結(jié)構(gòu)體所有字段不包含指針)或者只是單純分配塊內(nèi)存(makeslicecopy里分配一塊內(nèi)存然后再把數(shù)據(jù)copy進(jìn)去的時(shí)候會(huì)判斷element里包不包含指針,不包含的時(shí)候會(huì)傳nil給mallocgc),noscan就會(huì)被設(shè)置。

現(xiàn)在所有的疑問(wèn)都解決了:noscan是內(nèi)存分配時(shí)根據(jù)類(lèi)型信息來(lái)設(shè)置的;能跳過(guò)掃描的不只是map,符合條件的類(lèi)型不管是slice、map還是struct都可以。

優(yōu)化帶來(lái)的提升

說(shuō)了這么多,這個(gè)優(yōu)化帶來(lái)的提升有多少呢?

看個(gè)例子:

var a int64 = 1000
func generateIntSlice(n int64) []int64 {
ret := make([]int64, 0, n)
for i := int64(0); i < n; i++ {
ret = append(ret, a)
}
return ret
}
func generatePtrSlice(n int64) []*int64 {
ret := make([]*int64, 0, n)
for i := int64(0); i < n; i++ {
ret = append(ret, &a)
}
return ret
}
func BenchmarkGCScan1(b *testing.B) {
defer debug.SetGCPercent(debug.SetGCPercent(-1)) // 測(cè)試期間禁止自動(dòng)gc
for i := 0; i < b.N; i++ {
for j := 0; j < 20; j++ {
generatePtrSlice(10000)
}
runtime.GC()
}
}
func BenchmarkGCScan2(b *testing.B) {
defer debug.SetGCPercent(debug.SetGCPercent(-1))
for i := 0; i < b.N; i++ {
for j := 0; j < 20; j++ {
generateIntSlice(10000)
}
runtime.GC()
}
}

我們分別創(chuàng)建20個(gè)包含10000個(gè)int64或者*int64的slice(兩個(gè)類(lèi)型在x64系統(tǒng)上都是8字節(jié)大小),然后手動(dòng)觸發(fā)一次GC。為了讓結(jié)果更準(zhǔn)確,我們還在測(cè)試開(kāi)始前禁用了自動(dòng)觸發(fā)的gc,而且我們創(chuàng)建的slice的長(zhǎng)度和slice里元素的大小都是一樣的,所以總體來(lái)說(shuō)結(jié)果應(yīng)該比較接近真實(shí)的gc回收內(nèi)存時(shí)的性能。

這是結(jié)果:

goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
│ old.txt │ new.txt │
│ sec/op │ sec/op vs base │
GCScan-8 379.0μ ± 2% 298.0μ ± 2% -21.51% (p=0.000 n=10)
│ old.txt │ new.txt │
│ B/op │ B/op vs base │
GCScan-8 1.563Mi ± 0% 1.563Mi ± 0% ~ (p=0.438 n=10)
│ old.txt │ new.txt │
│ allocs/op │ allocs/op vs base │
GCScan-8 20.00 ± 0% 20.00 ± 0% ~ (p=1.000 n=10) 1
1 all samples are equal

內(nèi)存用量大家都一樣,但存指針的時(shí)候速度慢了五分之一。slice越大差距也會(huì)越大。可見(jiàn)跳過(guò)掃描帶來(lái)的提升還是很大的。

另外少用指針還有助于增加數(shù)據(jù)的局部性,不僅僅是惠及gc掃描。

如何利用這一優(yōu)化

最后我們看看如何利用這一優(yōu)化。

少用指針可以減輕gc壓力大家都知道,但有一些“不得不用”指針的時(shí)候。

以一個(gè)本地cache為例:

type Cache[K comparable, V any] struct {
m map[K]*V
}
func (c *Cache[K, V]) Get(key K) *V {
return c.m[key]
}
func (c *Cache[K, V]) Set(key Key, value *V) {
c.m[key] = value
}

值需要用指針是有兩個(gè)原因,一是map的元素不能取地址,如果我們想要cache里的數(shù)據(jù)可以自由使用的話那就不得不用臨時(shí)變量加復(fù)制,這樣如果我們想更新值的時(shí)候就會(huì)很麻煩;二是如果值很大的話復(fù)制帶來(lái)的開(kāi)銷(xiāo)會(huì)很大,用cache就是想提升性能呢反過(guò)來(lái)下降了怎么行。

但這么做就會(huì)導(dǎo)致Cache.m里的每一個(gè)鍵值對(duì)要被掃描,如果鍵值對(duì)很多的話性能會(huì)十分感人。

這樣看起來(lái)是“不得不用指針”的場(chǎng)景。真的是這樣嗎?考慮到cache本身就是空間換時(shí)間的做法,我們不妨再多用點(diǎn)空間:

type index = int
type Cache[K comparable, V any] struct {
buf []V
m map[K]index
}
func (c *Cache[K, V]) Get(key K) *V {
idx, ok := c.m[key]
if !ok {
return nil
}
return &c.buf[idx] // 可以對(duì)slice里存的數(shù)據(jù)取地址
}
func (c *Cache[K, V]) Set(key Key, value V) {
idx, ok := c.m[key]
if !ok {
// 新建
c.m[key] = len(c.buf)
c.buf = append(c.buf, value)
return
}
// 覆蓋已添加的
c.buf[idx] = value
}

我們用一個(gè)slice來(lái)存所有的值,然后再把key映射到值在slice中的索引上。對(duì)于slice的元素,我們是可以取地址的,因此可以簡(jiǎn)單拿到值的指針,對(duì)于值的更新也可以基于這個(gè)Get拿到的指針,時(shí)間復(fù)雜度不變,簡(jiǎn)單又方便。

然后我們?cè)賮?lái)看,現(xiàn)在buf和m都沒(méi)有指針了,只要K和V不包含指針,那么不管我們的cache里存了多少東西對(duì)gc來(lái)說(shuō)都只要看看外層的Cache對(duì)象是否存活就夠了。

但是這么做會(huì)有代價(jià):

Get會(huì)稍微慢一點(diǎn),因?yàn)椴粌H要做額外的檢查,還需要從兩個(gè)不同的數(shù)據(jù)結(jié)構(gòu)里拿數(shù)據(jù),對(duì)緩存不友好

存數(shù)據(jù)進(jìn)Cache的時(shí)候不可避免地需要一次復(fù)制

Get返回的指針沒(méi)有穩(wěn)定性,在底層的buf擴(kuò)容后就會(huì)失效

刪除元素會(huì)很慢,這怪我們用了slice而且需要維護(hù)map里的映射關(guān)系,解決方法倒是不少,比如你可以把待刪除元素和slice結(jié)尾的元素交換這樣slice里的其他元素不用移動(dòng)map也只要遍歷一次,又比如你可以再多浪費(fèi)點(diǎn)內(nèi)存用墓碑標(biāo)志來(lái)模擬刪除或者干脆不提供刪除功能(不好做就干脆不做,這是非常golang的做法)

順帶一提,前面說(shuō)的bigcache就利用了類(lèi)似的做法減輕了gc掃描壓力。

所以我建議先用benchmark和trace確定gc是性能瓶頸之后再進(jìn)行上面這樣的優(yōu)化,否則性能優(yōu)化不了還會(huì)帶來(lái)很多額外的煩惱。

總結(jié)

現(xiàn)在我們知道了對(duì)于不包含指針的對(duì)象,gc會(huì)跳過(guò)對(duì)它內(nèi)部子對(duì)象的掃描,這個(gè)優(yōu)化不止于map。

接口雖然看起來(lái)不像指針,但其實(shí)它內(nèi)部也有指針,因此接口是要被深入掃描的

另外還要強(qiáng)調(diào)一點(diǎn),這個(gè)只是官方版本的go做的優(yōu)化,不保證其他的編譯器實(shí)現(xiàn)比如gccgo、tinygo會(huì)有類(lèi)似的優(yōu)化。但少用指針減輕gc壓力是大多數(shù)語(yǔ)言的共識(shí),這點(diǎn)不會(huì)錯(cuò)。

最后的最后,還是老話,過(guò)早的優(yōu)化是萬(wàn)惡之源,但用這句話給自己低性能的設(shè)計(jì)找借口更是錯(cuò)上加錯(cuò),性能問(wèn)題靠數(shù)據(jù)說(shuō)話,多做benchmark少做白日夢(mèng)。

審核編輯:黃飛

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4308

    瀏覽量

    62445
  • 指針
    +關(guān)注

    關(guān)注

    1

    文章

    480

    瀏覽量

    70512
  • 編譯器
    +關(guān)注

    關(guān)注

    1

    文章

    1618

    瀏覽量

    49057
  • 數(shù)據(jù)統(tǒng)計(jì)

    關(guān)注

    0

    文章

    17

    瀏覽量

    7930

原文標(biāo)題:golang gc的內(nèi)部?jī)?yōu)化

文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Golang接口的作用和應(yīng)用場(chǎng)景

    Golang(Go)作為一門(mén)現(xiàn)代的靜態(tài)類(lèi)型編程語(yǔ)言,提供了許多強(qiáng)大的特性,其中之一便是接口(interface)。接口是Golang中的一個(gè)核心概念,它具有廣泛的應(yīng)用場(chǎng)景,可以幫助開(kāi)發(fā)者實(shí)現(xiàn)
    的頭像 發(fā)表于 12-05 10:44 ?1101次閱讀

    如何使用Golang連接MySQL

    首先我們來(lái)看如何使用Golang連接MySQL。
    的頭像 發(fā)表于 01-08 09:42 ?3266次閱讀
    如何使用<b class='flag-5'>Golang</b>連接MySQL

    SEO之內(nèi)部?jī)?yōu)化小結(jié)

    SEO之內(nèi)部?jī)?yōu)化小結(jié) 網(wǎng)站想提高排名,苦練內(nèi)力是必須的,因?yàn)閄P系統(tǒng)下載 2、title的重新定位  標(biāo)題中需要包含有優(yōu)化關(guān)鍵字的內(nèi)容,同時(shí)網(wǎng)站中的多個(gè)頁(yè)面標(biāo)題不能雷同,起碼要能顯示“關(guān)鍵字
    發(fā)表于 04-30 10:19

    Golang交叉編譯的使用備忘錄

    Golang 交叉編譯
    發(fā)表于 04-23 15:33

    Golang怎么實(shí)現(xiàn)UTS隔離

    Golang實(shí)現(xiàn)UTS隔離
    發(fā)表于 08-23 14:44

    Golang調(diào)用MySQL存儲(chǔ)過(guò)程解析

    Golang 調(diào)用MySQL存儲(chǔ)過(guò)程
    發(fā)表于 06-05 17:42

    汽車(chē)內(nèi)部常見(jiàn)指示圖標(biāo)

    汽車(chē)內(nèi)部常見(jiàn)指示圖標(biāo)   圖為汽車(chē)內(nèi)部常見(jiàn)指示圖案(一)
    發(fā)表于 03-11 15:47 ?7032次閱讀

    一種局部?jī)?yōu)化邊界的支持向量數(shù)據(jù)描述方法_陳君

    一種局部?jī)?yōu)化邊界的支持向量數(shù)據(jù)描述方法_陳君
    發(fā)表于 01-08 13:15 ?0次下載

    使用golang channel的諸多特性和技巧

    ? 本文介紹了使用 golang channel 的諸多特性和技巧,已經(jīng)熟悉了 go 語(yǔ)言特性的小伙伴也可以看看,很有啟發(fā)。
    的頭像 發(fā)表于 09-06 15:14 ?1794次閱讀
    使用<b class='flag-5'>golang</b> channel的諸多特性和技巧

    golang123基于Go的開(kāi)源社區(qū)系統(tǒng)

    ./oschina_soft/golang123.zip
    發(fā)表于 06-10 09:51 ?1次下載
    <b class='flag-5'>golang</b>123基于Go的開(kāi)源社區(qū)系統(tǒng)

    初探Golang內(nèi)聯(lián)

    今天我們來(lái)聊聊 Golang 中的內(nèi)聯(lián)。
    的頭像 發(fā)表于 12-13 09:51 ?912次閱讀

    GoLang的安裝和使用

    GoLang的安裝和使用
    的頭像 發(fā)表于 01-13 14:06 ?1246次閱讀
    <b class='flag-5'>GoLang</b>的安裝和使用

    一個(gè)快速應(yīng)用程序開(kāi)發(fā)(RAD)工具(Golang版)

    SNMPAgent Builder(Golang版)是一個(gè)快速應(yīng)用程序開(kāi)發(fā)(RAD)工具,用于基于Golang 的 SNMP代理開(kāi)發(fā)。提供了一個(gè)直觀的圖形用戶(hù)界面,用于自動(dòng)執(zhí)行各種SNMP 代理開(kāi)發(fā)任務(wù)
    的頭像 發(fā)表于 04-13 09:30 ?1497次閱讀

    介紹一種KV存儲(chǔ)的GC優(yōu)化實(shí)踐

    內(nèi)部需求出發(fā),我們基于TiKV設(shè)計(jì)了一款兼容Redis的KV存儲(chǔ)。基于TiKV的數(shù)據(jù)存儲(chǔ)機(jī)制,對(duì)于窗口數(shù)據(jù)的處理以及過(guò)期數(shù)據(jù)的GC問(wèn)題卻成為一個(gè)難題。
    的頭像 發(fā)表于 05-16 09:33 ?774次閱讀
    介紹一種KV存儲(chǔ)的<b class='flag-5'>GC</b><b class='flag-5'>優(yōu)化</b>實(shí)踐

    【芒果派MangoPi MQ Quad】使用Golang點(diǎn)燈

    使用Golang在芒果派上點(diǎn)燈
    的頭像 發(fā)表于 07-21 14:44 ?673次閱讀
    【芒果派MangoPi MQ Quad】使用<b class='flag-5'>Golang</b>點(diǎn)燈