Go內(nèi)存節(jié)省技巧簡(jiǎn)單實(shí)現(xiàn)方法
正文
除非您正在對(duì)服務(wù)進(jìn)行原型設(shè)計(jì),否則您可能會(huì)關(guān)心應(yīng)用程序的內(nèi)存使用情況。占用更小的內(nèi)存,會(huì)使基礎(chǔ)設(shè)施成本降低,擴(kuò)展變得更容易。盡管 Go 以不消耗大量?jī)?nèi)存而聞名,但仍有一些方法可以進(jìn)一步減少消耗。其中一些需要大量重構(gòu),但很多都很容易做到。
預(yù)先分配切片
要理解這種優(yōu)化,我們必須了解切片在 Go 中是如何工作的,為此我們必須首先了解數(shù)組。
go.dev 上有一篇非常好的關(guān)于這個(gè)主題的文章。
數(shù)組是具有連續(xù)內(nèi)存的相同類型的集合。數(shù)組類型定義時(shí)要指定長(zhǎng)度和元素類型。
因?yàn)閿?shù)組的長(zhǎng)度是它們類型的一部分,數(shù)組的主要問題是它們大小固定,不能調(diào)整。
與數(shù)組類型不同,切片類型無需指定長(zhǎng)度。切片的聲明方式與數(shù)組相同,但沒有數(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)致不必要的內(nèi)存分配和 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: 0xc000018030, Length: 1, Capacity: 1, Values: [0]
Address: 0xc000018050, Length: 2, Capacity: 2, Values: [0 1]
Address: 0xc000082020, Length: 3, Capacity: 4, Values: [0 1 2]
Address: 0xc000082020, Length: 4, Capacity: 4, Values: [0 1 2 3]
Address: 0xc000084040, Length: 5, Capacity: 8, Values: [0 1 2 3 4]
憑借輸出結(jié)果我們可以得出結(jié)論,無論何時(shí)必須增加容量(增加 2 倍),都必須創(chuàng)建一個(gè)新的底層數(shù)組(新的內(nèi)存地址)并將值復(fù)制到新數(shù)組中。
有趣是,當(dāng)容量<1024 時(shí)會(huì)漲為之前的 2 倍,當(dāng)容量>=1024時(shí)會(huì)以 1.25 倍增長(zhǎng)。從 Go 1.18 開始,這已經(jīng)變得更加線性。
Address:?0xc000018030,?Length:?1,?Capacity:?1,?Values:?[0] Address:?0xc000018050,?Length:?2,?Capacity:?2,?Values:?[0?1] Address:?0xc000082020,?Length:?3,?Capacity:?4,?Values:?[0?1?2] Address:?0xc000082020,?Length:?4,?Capacity:?4,?Values:?[0?1?2?3] Address:?0xc000084040,?Length:?5,?Capacity:?8,?Values:?[0?1?2?3?4]func?BenchmarkAppend(b?*testing.B)?{ ????var?ints?[]int ????for?i?:=?0;?i?<?b.N;?i++?{ ????????ints?=?append(ints,?i) ????} } func?BenchmarkPreallocAssign(b?*testing.B)?{ ????ints?:=?make([]int,?b.N) ????for?i?:=?0;?i?<?b.N;?i++?{ ????????ints[i]?=?i ????} } func?BenchmarkAppend(b?*testing.B)?{ ????var?ints?[]int ????for?i?:=?0;?i?<?b.N;?i++?{ ????????ints?=?append(ints,?i) ????} } func?BenchmarkPreallocAssign(b?*testing.B)?{ ????ints?:=?make([]int,?b.N) ????for?i?:=?0;?i?<?b.N;?i++?{ ????????ints[i]?=?i ????} }
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è)工具有助于切片的預(yù)分配:
- prealloc: 一個(gè)靜態(tài)分析工具,用于查找可能被預(yù)分配的切片聲明。
- makezero: 一個(gè)靜態(tài)分析工具,用于查找未以零長(zhǎng)度初始化且稍后有追加的切片聲明。
結(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)) }
上述的輸出為 96 字節(jié),而所有字段相加為 82 字節(jié)。那額外的 14 個(gè)字節(jié)是來自哪里呢?
現(xiàn)代 64 位 CPU 以 64 位(8 字節(jié))的塊獲取數(shù)據(jù)。如果我們有一個(gè)較舊的 32 位 CPU,它將以 32 位(4 字節(jié))的塊進(jìn)行。
第一個(gè)周期占用 8 個(gè)字節(jié),拉取“IsDraft”字段占用了 1 個(gè)字節(jié)并且產(chǎn)生 7 個(gè)未使用字節(jié)。它不能占用“一半”的字段。
第二個(gè)和第三個(gè)周期取 Title 字符串,第四個(gè)周期取 ID,依此類推。到取 IsDeleted 字段時(shí),它使用 1 個(gè)字節(jié)并有 7 個(gè)字節(jié)未使用。
對(duì)內(nèi)存節(jié)省的關(guān)鍵是按字段占用大小從上到下對(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 ????IsDraft?????bool??????//?1?byte ????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 個(gè)字節(jié)
- float32: 4 個(gè)字節(jié)
- byte: 1 個(gè)字節(jié)
不需要手動(dòng)檢查您的結(jié)構(gòu)體并按大小對(duì)其進(jìn)行排序,而是使用 工具找到這些結(jié)構(gòu)并報(bào)告“正確”的排序。
- maligned - 已棄用,用于報(bào)告未對(duì)齊的結(jié)構(gòu)并打印出正確排序的字段。它在一年前被棄用,但您仍然可以安裝舊版本并使用它。
- govet/fieldalignment: gotools 和
govet
的一部分工具,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>
對(duì)上面的代碼使用 govet/fieldalignment:
fieldalignment:?struct?of?size?96?could?be?88?(govet)
使用 map[string]struct{} 而不是 map[string]bool
Go 沒有內(nèi)置的集合,通常使用 map[string]bool{}
表示集合。盡管它更具可讀性(這非常重要),但將其作為一個(gè)集合使用是錯(cuò)誤的,因?yàn)樗哂袃煞N狀態(tài)(假/真)并且與空結(jié)構(gòu)體相比使用了額外的內(nèi)存。
空結(jié)構(gòu)體 (struct{}
) 是沒有額外字段的結(jié)構(gòu)類型,占用零字節(jié)的存儲(chǔ)空間。Dave Chaney 有一篇關(guān)于空結(jié)構(gòu)的詳細(xì)博客 。
除非您的 map/set 包含大量值并且需要獲得額外的內(nèi)存,否則我建議使用 map[string]struct{}
。
100 000 000 次 map 寫入的極端示例:
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{}{} ????} }
多次運(yùn)行程序得到的結(jié)果一致(MBP 14 2021,10C M1 Pro):
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 }
以上就是Go內(nèi)存節(jié)省技巧簡(jiǎn)單實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于Go 內(nèi)存節(jié)省的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang文件服務(wù)器的兩種方式(可以訪問任何目錄)
這篇文章主要介紹了golang文件服務(wù)器的兩種方式,可以訪問任何目錄,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04搭建Go語言的ORM框架Gorm的具體步驟(從Java到go)
很多朋友不知道如何使用Goland軟件,搭建一個(gè)ORM框架GORM,今天小編給大家分享一篇教程關(guān)于搭建Go語言的ORM框架Gorm的具體步驟(從Java到go),感興趣的朋友跟隨小編一起學(xué)習(xí)下吧2022-09-09goland?導(dǎo)入github包報(bào)紅問題解決
本文主要介紹了Go項(xiàng)目在GoLand中導(dǎo)入依賴標(biāo)紅問題解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08go語言實(shí)戰(zhàn)之實(shí)現(xiàn)比特幣地址校驗(yàn)步驟
這篇文章主要介紹了go語言實(shí)戰(zhàn)之實(shí)現(xiàn)比特幣地址校驗(yàn)步驟,利用生產(chǎn)的隨機(jī)數(shù)采用橢圓加密算法生成公鑰,具體步驟實(shí)例代碼請(qǐng)參考下本文2021-05-05詳解如何為Go中的無限循環(huán)添加時(shí)間限制
在 Go 語言的開發(fā)過程中,我們有時(shí)需要在后臺(tái)執(zhí)行長(zhǎng)時(shí)間運(yùn)行的任務(wù),例如監(jiān)聽或輪詢某些資源,這篇文章將通過一個(gè)實(shí)例詳細(xì)介紹如何為 Go 語言中的無限循環(huán)設(shè)置時(shí)間限制,保證程序的健壯性和可控性,需要的朋友可以參考下2024-04-04Go 自定義package包設(shè)置與導(dǎo)入操作
這篇文章主要介紹了Go 自定義package包設(shè)置與導(dǎo)入操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-05-05