Golang 內(nèi)存管理簡(jiǎn)單技巧詳解
引言
除非您正在對(duì)服務(wù)進(jìn)行原型設(shè)計(jì),否則您可能會(huì)關(guān)心應(yīng)用程序的內(nèi)存使用情況。內(nèi)存占用更小,基礎(chǔ)設(shè)施成本降低,擴(kuò)展變得更容易/延遲。
盡管 Go 以不消耗大量?jī)?nèi)存而聞名,但仍有一些方法可以進(jìn)一步減少消耗。其中一些需要大量重構(gòu),但很多都很容易做到。
預(yù)先分配切片
數(shù)組是具有連續(xù)內(nèi)存的相同類型的集合。數(shù)組類型定義指定長(zhǎng)度和元素類型。數(shù)組的主要問題是它們的大小是固定的——它們不能調(diào)整大小,因?yàn)閿?shù)組的長(zhǎng)度是它們類型的一部分。
與數(shù)組類型不同,切片類型沒有指定長(zhǎng)度。切片的聲明方式與數(shù)組相同,但沒有元素計(jì)數(shù)。
切片是數(shù)組的包裝器,它們不擁有任何數(shù)據(jù)——它們是對(duì)數(shù)組的引用。它們由指向數(shù)組的指針、段的長(zhǎng)度及其容量(底層數(shù)組中的元素?cái)?shù))組成。
當(dāng)您追加到一個(gè)沒有新值容量的切片時(shí) - 會(huì)創(chuàng)建一個(gè)具有更大容量的新數(shù)組,并將當(dāng)前數(shù)組中的值復(fù)制到新數(shù)組中。這會(huì)導(dǎo)致不必要的分配和 CPU 周期。
為了更好地理解這一點(diǎn),讓我們看一下以下代碼段:
func main() { var ints []int for i := 0; i < 5; i++ { ints = append(ints, i) fmt.Printf("Address: %p, Length: %d, Capacity: %d, Values: %v\n", ints, len(ints), cap(ints), ints) } }
輸出如下:
Address: 0xc0000160c8, Length: 1, Capacity: 1, Values: [0]
Address: 0xc0000160f0, Length: 2, Capacity: 2, Values: [0 1]
Address: 0xc00001e080, Length: 3, Capacity: 4, Values: [0 1 2]
Address: 0xc00001e080, Length: 4, Capacity: 4, Values: [0 1 2 3]
Address: 0xc00001a140, Length: 5, Capacity: 8, Values: [0 1 2 3 4]
查看輸出,我們可以得出結(jié)論,無論何時(shí)必須增加容量(增加 2 倍),都必須創(chuàng)建一個(gè)新的底層數(shù)組(新的內(nèi)存地址)并將值復(fù)制到新數(shù)組中。
有趣的事實(shí)是,容量增長(zhǎng)的因素曾經(jīng)是容量 <1024 的 2 倍,以及 >= 1024 的 1.25 倍。從 Go 1.18 開始,這已經(jīng)變得更加線性。
name time/op Append-10 3.81ns ± 0% PreallocAssign-10 0.41ns ± 0% name alloc/op Append-10 45.0B ± 0% PreallocAssign-10 8.00B ± 0% name allocs/op Append-10 0.00 PreallocAssign-10 0.00
查看上述基準(zhǔn),我們可以得出結(jié)論,將值分配給預(yù)分配的切片和將值附加到切片之間存在很大差異。
兩個(gè) linter 有助于預(yù)分配切片:
- prealloc: 一種靜態(tài)分析工具,用于查找可能被預(yù)分配的切片聲明。
- makezero: 一種靜態(tài)分析工具,用于查找未以零長(zhǎng)度初始化且稍后與 append 一起使用的切片聲明。
結(jié)構(gòu)中的順序字段
您之前可能沒有想到這一點(diǎn),但結(jié)構(gòu)中字段的順序?qū)?nèi)存消耗很重要。
以下面的結(jié)構(gòu)為例:
type Post struct { IsDraft bool // 1 byte Title string // 16 bytes ID int64 // 8 bytes Description string // 16 bytes IsDeleted bool // 1 byte Author string // 16 bytes CreatedAt time.Time // 24 bytes } func main(){ p := Post{} fmt.Println(unsafe.Sizeof(p)) }
上述函數(shù)的輸出為 96(字節(jié)),而所有字段相加為 82 字節(jié)。額外的 14 個(gè)字節(jié)來自哪里?
現(xiàn)代 64 位 CPU 以 64 位(8 字節(jié))塊的形式獲取數(shù)據(jù)。如果我們有一個(gè)較舊的 32 位 CPU,它將執(zhí)行 32 位(4 字節(jié))的塊。
第一個(gè)周期占用 8 個(gè)字節(jié),IsDraft
字段占用 1 個(gè)字節(jié),并有 7 個(gè)未使用字節(jié)。它不能占據(jù)一個(gè)字段的“一半”。
第二和第三個(gè)循環(huán)取 Title
字符串,第四個(gè)循環(huán)取 ID
,依此類推。再次使用 IsDeleted
字段,它需要 1 個(gè)字節(jié)并有 7 個(gè)未使用的字節(jié)。
真正重要的是按字段的大小從上到下對(duì)字段進(jìn)行排序。對(duì)上述結(jié)構(gòu)進(jìn)行排序,大小減少到 88 個(gè)字節(jié)。最后兩個(gè)字段 IsDraft
和 IsDeleted
被放在同一個(gè)塊中,從而將未使用的字節(jié)數(shù)從 14 (2x7) 減少到 6 (1 x 6),在此過程中節(jié)省了 8 個(gè)字節(jié)。
type Post struct { CreatedAt time.Time // 24 bytes Title string // 16 bytes Description string // 16 bytes Author string // 16 bytes ID int64 // 8 bytes IsDeleted bool // 1 byte } func main(){ p := Post{} fmt.Println(unsafe.Sizeof(p)) }
在 64 位架構(gòu)上占用 <8 字節(jié)的 Go 類型:
- bool:1 個(gè)字節(jié)
- int8/uint8:1 個(gè)字節(jié)
- int16/uint16:2 個(gè)字節(jié)
- int32/uint32/rune:4 字節(jié)
- float32:4 字節(jié)
- byte:1個(gè)字節(jié)
無需手動(dòng)檢查結(jié)構(gòu)并按大小對(duì)其進(jìn)行排序,而是使用 linter 找到這些結(jié)構(gòu)并(用于)報(bào)告“正確”排序。
- maligned: 不推薦使用的 linter,用于報(bào)告未對(duì)齊的結(jié)構(gòu)并打印出正確排序的字段。它在一年前被棄用,但您仍然可以安裝舊版本并使用它。
- govet/fieldalignment: 作為 gotools 和 govet linter 的一部分,fieldalignment 打印出未對(duì)齊的結(jié)構(gòu)和結(jié)構(gòu)的當(dāng)前/理想大小。
要安裝和運(yùn)行 fieldalignment:
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest fieldalignment -fix <package_path>
在上面的代碼中使用 govet/fieldalignment:
fieldalignment: struct of size 96 could be 88 (govet)
使用 map[string]struct{} 而不是 map[string]bool
Go 沒有內(nèi)置集合,通常使用 map[string]bool{}
來表示集合。盡管它更具可讀性,這一點(diǎn)非常重要,但將其作為一個(gè)集合使用是錯(cuò)誤的,因?yàn)樗袃煞N狀態(tài)(假/真),并且與空結(jié)構(gòu)相比使用了額外的內(nèi)存。
空結(jié)構(gòu)體 (struct{}
) 是沒有額外字段的結(jié)構(gòu)體類型,占用零字節(jié)存儲(chǔ)空間。
我不建議這樣做,除非您的 map/set 包含大量值并且您需要獲得額外的內(nèi)存或者您正在為低內(nèi)存平臺(tái)進(jìn)行開發(fā)。
使用 100 000 000 次寫入地圖的極端示例:
func BenchmarkBool(b *testing.B) { m := make(map[uint]bool) for i := uint(0); i < 100_000_000; i++ { m[i] = true } } func BenchmarkEmptyStruct(b *testing.B) { m := make(map[uint]struct{}) for i := uint(0); i < 100_000_000; i++ { m[i] = struct{}{} } }
得到以下結(jié)果,在整個(gè)運(yùn)行過程中非常一致:
name time/op Bool 12.4s ± 0% EmptyStruct 12.0s ± 0% name alloc/op Bool 3.78GB ± 0% EmptyStruct 3.43GB ± 0% name allocs/op Bool 3.91M ± 0% EmptyStruct 3.90M ± 0%
使用這些數(shù)字,我們可以得出結(jié)論,使用空結(jié)構(gòu)映射的寫入速度提高了 3.2%,分配的內(nèi)存減少了 10%。
此外,使用 map[type]struct{}
是實(shí)現(xiàn)集合的正確解決方法,因?yàn)槊總€(gè)鍵都有一個(gè)值。使用 map[type]bool
,每個(gè)鍵都有兩個(gè)可能的值,這不是一個(gè)集合,如果目標(biāo)是創(chuàng)建一個(gè)集合,則可能會(huì)被誤用。
然而,可讀性大多數(shù)時(shí)候比(可忽略的)內(nèi)存改進(jìn)更重要。與空結(jié)構(gòu)體相比,使用布爾值更容易掌握查找:
m := make(map[string]bool{}) if m["key"]{ // Do something } v := make(map[string]struct{}{}) if _, ok := v["key"]; ok{ // Do something }
參考鏈接:Easy memory-saving tricks in Go | Emir Ribic (ribice.ba)
以上就是Golang 內(nèi)存管理簡(jiǎn)單技巧詳解的詳細(xì)內(nèi)容,更多關(guān)于Golang 內(nèi)存管理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang中的sync.WaitGroup用法實(shí)例
這篇文章主要介紹了Golang中的sync.WaitGroup用法實(shí)例,WaitGroup的用途,它能夠一直等到所有的goroutine執(zhí)行完成,并且阻塞主線程的執(zhí)行,直到所有的goroutine執(zhí)行完成,需要的朋友可以參考下2015-07-07適合PHP同學(xué)的GoFrame框架使用體驗(yàn)及學(xué)習(xí)建議
這篇文章主要為大家介紹了非常適合PHP同學(xué)使用的GoFrame框架設(shè)計(jì)思想使用體驗(yàn)及學(xué)習(xí)建議介紹,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Go語言實(shí)現(xiàn)Base64、Base58編碼與解碼
本文主要介紹了Base64、Base58編碼與解碼,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07golang之?dāng)?shù)據(jù)校驗(yàn)的實(shí)現(xiàn)代碼示例
這篇文章主要介紹了golang之?dāng)?shù)據(jù)校檢的實(shí)現(xiàn)代碼示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10Go語言安裝和GoLand2021最全超詳細(xì)安裝教程
Go語言和GoLand的關(guān)系好比于java和idea、python和pycharm,因此我們需要先安裝好Go語言后才能安裝GoLand。它的安裝和java,python的安裝大同小異,好了,下面給大家?guī)砹薌oLand2021安裝教程,需要的朋友參考下吧2021-08-08