色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

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

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

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

常見golang gc的內(nèi)部優(yōu)化方案

馬哥Linux運維 ? 來源:cnblogs ? 2024-03-29 11:19 ? 次閱讀

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

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

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

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

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

于是這篇文章誕生了。

跳過掃描指的是什么

前置知識少不得。

簡單的說,gc在檢查對象是否存活的時候,除了對象本身,還要檢查對象的子對象是否引用了其他對象,具體來說:

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

map的子對象就是里面存的鍵和值了

struct的子對象是它的每一個字段

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

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

什么樣的對象是可以跳過掃描的

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

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

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

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

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

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

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

markrootSpans是用來處理那些存放設(shè)置了終結(jié)器的對象的內(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é)器的對象
// 這里檢查這個對象占用的內(nèi)存上是否設(shè)置了跳過掃描的標(biāo)記
// 設(shè)置了的話就不要繼續(xù)掃描對象自己的子對象了
if !s.spanclass.noscan() {
scanobject(p, gcw)
}
// 這個span本身就是root對象,所以剩下的直接用scanblock處理
scanblock(uintptr(unsafe.Pointer(&spf.fn)), goarch.PtrSize, &oneptrmask[0], gcw, nil)
}
unlock(&s.speciallock)
}
}
}

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

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

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

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

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

這個函數(shù)長歸長,條理還是清晰的:

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

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

因為這個函數(shù)本身已經(jīng)是在掃描了,所以不太會有“跳過”的相關(guān)的邏輯,而且你也看到了把這個函數(shù)放在不需要掃描子對象的對象上調(diào)用時會觸發(fā)throw,throw會導(dǎo)致程序報錯并退出執(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)記為不需要進一步掃描,則會跳過后續(xù)的流程(內(nèi)存會被放進gc掃描的工作隊列里等著被取出來掃描)
if span.spanclass.noscan() {
...
return
}
}
// 對象被放進工作隊列等待掃描
}

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

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

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

跳過掃描的具體流程

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

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

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

A的掃描流程是這樣的:

gc開始運行,先標(biāo)記root對象

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

假設(shè)A設(shè)置了終結(jié)器,又因為A是可跳過掃描子對象的,因此markrootSpan會直接調(diào)用scanblock

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

因為A可跳過掃描,所以greyobject做完標(biāo)記就返回了,A不會進入工作隊列

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

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

因為B并不是root對象,且不可跳過掃描,所以它作為某個root對象的子對象,現(xiàn)在肯定在gc工作隊列里

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

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

scanobject把每個B的子對象都用greyobject處理,C也不例外

因為C可跳過掃描,所以greyobject做完標(biāo)記就返回了,C不會進入工作隊列

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

這樣基本涵蓋了所有的情況,一些我沒單獨說的比如“可跳過對象E是不可跳過root對象D的子對象”這樣的情況,實際上和情況2沒什么區(qū)別。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

那么這個mallocgc又是誰調(diào)用的?答案太多了,因為new,make都會用到它。我們用slice和map做例子看看。

首先是slice。這個非常簡單,創(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中的元素的類型信息被傳給了mallocgc。如果slice的元素不包含指針,那么slice是可以跳過掃描的。

map比較特殊,跳過掃描的是它的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() 返回鍵值對中是否包含指針
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)
}

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

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

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

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

說了這么多,這個優(yōu)化帶來的提升有多少呢?

看個例子:

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)) // 測試期間禁止自動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個包含10000個int64或者*int64的slice(兩個類型在x64系統(tǒng)上都是8字節(jié)大?。?,然后手動觸發(fā)一次GC。為了讓結(jié)果更準(zhǔn)確,我們還在測試開始前禁用了自動觸發(fā)的gc,而且我們創(chuàng)建的slice的長度和slice里元素的大小都是一樣的,所以總體來說結(jié)果應(yīng)該比較接近真實的gc回收內(nèi)存時的性能。

這是結(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)存用量大家都一樣,但存指針的時候速度慢了五分之一。slice越大差距也會越大??梢娞^掃描帶來的提升還是很大的。

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

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

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

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

以一個本地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
}

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

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

這樣看起來是“不得不用指針”的場景。真的是這樣嗎?考慮到cache本身就是空間換時間的做法,我們不妨再多用點空間:

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] // 可以對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
}

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

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

但是這么做會有代價:

Get會稍微慢一點,因為不僅要做額外的檢查,還需要從兩個不同的數(shù)據(jù)結(jié)構(gòu)里拿數(shù)據(jù),對緩存不友好

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

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

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

順帶一提,前面說的bigcache就利用了類似的做法減輕了gc掃描壓力。

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

總結(jié)

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

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

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

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

審核編輯:黃飛

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

    關(guān)注

    3

    文章

    4327

    瀏覽量

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

    關(guān)注

    1

    文章

    480

    瀏覽量

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

    關(guān)注

    1

    文章

    1623

    瀏覽量

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

    關(guān)注

    0

    文章

    17

    瀏覽量

    7934

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

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

收藏 人收藏

    評論

    相關(guān)推薦

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

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

    如何使用Golang連接MySQL

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

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

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

    Golang交叉編譯的使用備忘錄

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

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

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

    Golang調(diào)用MySQL存儲過程解析

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

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

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

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

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

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

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

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

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

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

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

    GoLang的安裝和使用

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

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

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

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

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

    【芒果派MangoPi MQ Quad】使用Golang點燈

    使用Golang在芒果派上點燈
    的頭像 發(fā)表于 07-21 14:44 ?692次閱讀
    【芒果派MangoPi MQ Quad】使用<b class='flag-5'>Golang</b>點燈
    主站蜘蛛池模板: 爽死你个放荡粗暴小淫货漫画| 精品国产免费观看久久久| 亚洲成av人影院| 黄页网站18以下勿看免费| 在线免费视频a| 暖暖 日本 视频 在线观看免费 | 日本高清天码一区在线播放| 公和熄洗澡三级中文字幕| 午夜一个人在线观看完整版| 激情男女高潮射精AV免费| 在线观看永久免费网址| 女人张开腿让男人添| 国产Av影片麻豆精品传媒| 亚洲AV无码乱码A片无码蜜桃| 精品无码国产自产在线观看水浒传 | 国产在线观看网址你懂得| 亚洲无遮挡| 男人边吃奶边挵进去呻吟漫画| 成年女人色毛片免费| 小夫妻天天恶战| 久久久无码精品无码国产人妻丝瓜| 99精品热视频30在线热视频 | 好吊妞在线成人免费| 中文字幕亚洲无限码| 青草在线在线d青草在线| 国产国语在线播放视频| 亚洲中文字幕手机版| 女性酥酥影院| 国产免费高清mv视频在线观看| 伊人影院综合| 秋霞电影网午夜免费鲁丝片| 国产午夜精品久久理论片 | 久久亚洲精品永久网站| www国产av偷拍在线播放| 性XXXXX搡XXXXX搡景甜| 久青草国产观看在线视频| 哒哒哒高清视频在线观看| 亚洲乱码AV久久久久久久| 欧美97色伦综合网| 国产精品色午夜视频免费看| 最新无码国产在线视频2020|