Go語言中節(jié)省內(nèi)存技巧方法示例
引言
GO雖然不消耗大量內(nèi)存,但是仍有一些小技巧可以節(jié)省內(nèi)存,良好的編碼習(xí)慣是每一個程序員都應(yīng)該具備的素質(zhì)。
預(yù)先分配切片
數(shù)組是具有連續(xù)內(nèi)存的相同類型的集合。數(shù)組類型定義時要指定長度和元素類型。
因為數(shù)組的長度是它們類型的一部分,數(shù)組的主要問題是它們大小固定,不能調(diào)整。
與數(shù)組類型不同,切片類型無需指定長度。切片的聲明方式與數(shù)組相同,但沒有數(shù)量元素。
切片是數(shù)組的包裝器,它們不擁有任何數(shù)據(jù)——它們是對數(shù)組的引用。它們由指向數(shù)組的指針、長度及其容量(底層數(shù)組中的元素數(shù))組成。
當(dāng)您向沒有足夠容量的切片添加一個新值時 - 會創(chuàng)建一個具有更大容量的新數(shù)組,并將當(dāng)前數(shù)組中的值復(fù)制到新數(shù)組中。這會導(dǎo)致不必要的內(nèi)存分配和 CPU 周期。
為了更好地理解這一點,讓我們看一下以下代碼段:
func main() { var ints []int fmt.Printf("Address: %p, Length: %d, Capacity: %d, Values: %v\n", ints, len(ints), cap(ints), ints) 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) } }
結(jié)果
Address: 0x0, Length: 0, Capacity: 0, Values: []
Address: 0xc0000160d0, Length: 1, Capacity: 1, Values: [0]
Address: 0xc0000160e0, Length: 2, Capacity: 2, Values: [0 1]
Address: 0xc000020100, Length: 3, Capacity: 4, Values: [0 1 2]
Address: 0xc000020100, Length: 4, Capacity: 4, Values: [0 1 2 3]
Address: 0xc00001a180, Length: 5, Capacity: 8, Values: [0 1 2 3 4]
可以看到第一次聲明數(shù)組var ints []int
的時候,是不給它分配內(nèi)存的,內(nèi)存地址為0,大小和容量也都是0 后面每次擴容都是2的倍數(shù),并且每次擴容內(nèi)存地址都發(fā)生了改變。
當(dāng)容量<1024 時會漲為之前的 2 倍,當(dāng)容量>=1024時會以 1.25 倍增長。從 Go 1.18 開始,這已經(jīng)變得更加線性
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) } }
結(jié)果如下
goos: darwin
goarch: amd64
pkg: mygo
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkPreallocAssign-12 321257311 3.609 ns/op 8 B/op 0 allocs/op
BenchmarkAppend-12 183322678 12.37 ns/op 42 B/op 0 allocs/op
PASS
ok mygo 6.236s
由上述基準(zhǔn),我們可以得出結(jié)論,將值分配給預(yù)分配的切片和將值追加到切片之間是存在很大差異的。預(yù)先分配大小可以提速3倍多,而且內(nèi)存分配也更小。
結(jié)構(gòu)體中的字段順序
以下面結(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 個字節(jié)是來自哪里呢?
現(xiàn)代 64 位 CPU 以 64 位(8 字節(jié))的塊獲取數(shù)據(jù)
第一個周期占用 8 個字節(jié),拉取“IsDraft”字段占用了 1 個字節(jié)并且產(chǎn)生 7 個未使用字節(jié)。它不能占用“一半”的字段。
第二個和第三個周期取 Title 字符串,第四個周期取 ID,依此類推。到取 IsDeleted 字段時,它使用 1 個字節(jié)并有 7 個字節(jié)未使用。
對內(nèi)存節(jié)省的關(guān)鍵是按字段占用大小從上到下對字段進行排序。對上述結(jié)構(gòu)進行排序,大小可減少到 88 個字節(jié)。最后兩個字段 IsDraft 和 IsDeleted 被放在同一個塊中,從而將未使用的字節(jié)數(shù)從 14 (2x7) 減少到 6 (1 x 6),在此過程中節(jié)省了 8 個字節(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)) }
上述的輸出為 88 字節(jié)
極端情況
type Post struct { IsDraft bool // 1 byte I64 int64 // 8 bytes IsDraft1 bool // 1 byte I641 int64 // 8 bytes IsDraft2 bool // 1 byte I642 int64 // 8 bytes IsDraft3 bool // 1 byte I643 int64 // 8 bytes IsDraft4 bool // 1 byte I644 int64 // 8 bytes IsDraft5 bool // 1 byte I645 int64 // 8 bytes IsDraft6 bool // 1 byte I646 int64 // 8 bytes IsDraft7 bool // 1 byte I647 int64 // 8 bytes } type Post1 struct { IsDraft bool // 1 byte IsDraft1 bool // 1 byte IsDraft2 bool // 1 byte IsDraft3 bool // 1 byte IsDraft4 bool // 1 byte IsDraft5 bool // 1 byte IsDraft6 bool // 1 byte IsDraft7 bool // 1 byte I64 int64 // 8 bytes I641 int64 // 8 bytes I642 int64 // 8 bytes I643 int64 // 8 bytes I644 int64 // 8 bytes I645 int64 // 8 bytes I646 int64 // 8 bytes I647 int64 // 8 bytes }
第一個結(jié)構(gòu)體占用128字節(jié),第二個結(jié)構(gòu)體占用72字節(jié)。節(jié)省空間:(128-72)/129=43.75%.
在 64 位架構(gòu)上占用小于 8 字節(jié)的 Go 類型:
- bool: 1 個字節(jié)
- int8/uint8: 1 個字節(jié)
- int16/uint16: 2 個字節(jié)
- int32/uint32/rune: 4 個字節(jié)
- float32: 4 個字節(jié)
- byte: 1 個字節(jié)
使用 map[string]struct{} 而不是 map[string]bool
Go 沒有內(nèi)置的集合,通常使用 map[string]bool{}
表示集合。盡管它更具可讀性(這非常重要),但將其作為一個集合使用是錯誤的,因為它具有兩種狀態(tài)(假/真)并且與空結(jié)構(gòu)體相比使用了額外的內(nèi)存。
空結(jié)構(gòu)體 (struct{}
) 是沒有額外字段的結(jié)構(gòu)類型,占用零字節(jié)的存儲空間。
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é)果
goos: darwin
goarch: amd64
pkg: mygo
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkBool-12 1 24052439603 ns/op 3766222824 B/op 3902813 allocs/op
BenchmarkEmptyStruct-12 1 22450213018 ns/op 3418648448 B/op 3903556 allocs/op
PASS
ok mygo 46.937s
可以看到執(zhí)行速度提升了一些,但是效果不太明顯。
使用bool值有個好處是查找的時候更方便,從map中取值只需要判斷一個值就行了,而使用空結(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 }
參考
【2】Easy memory-saving tricks in Go
以上就是Go語言中節(jié)省內(nèi)存技巧方法示例的詳細內(nèi)容,更多關(guān)于Go語言節(jié)省內(nèi)存技巧的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go1.18?新特性之多模塊Multi-Module工作區(qū)模式
這篇文章主要介紹了Go1.18?新特性之多模塊Multi-Module工作區(qū)模式,在 Go 1.18之前,建議使用依賴模塊中的 replace 指令來處理這個問題,從 Go 1.18開始引入了一種同時處理多個模塊的新方法,通過案例給大家詳細介紹,感興趣的朋友一起看看吧2022-04-04go語言實現(xiàn)Elasticsearches批量修改查詢及發(fā)送MQ操作示例
這篇文章主要為大家介紹了go語言實現(xiàn)Elasticsearches批量修改查詢及發(fā)送MQ操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04Go語言實現(xiàn)單端口轉(zhuǎn)發(fā)到多個端口
這篇文章主要為大家詳細介紹了Go語言實現(xiàn)單端口轉(zhuǎn)發(fā)到多個端口,文中的示例代碼講解詳細,具有一定的參考價值,對大家的學(xué)習(xí)或工作有一定的幫助,需要的小伙伴可以了解下2024-02-02golang 實現(xiàn)一個負載均衡案例(隨機,輪訓(xùn))
這篇文章主要介紹了golang 實現(xiàn)一個負載均衡案例(隨機、輪訓(xùn)),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04Go 通過結(jié)構(gòu)struct實現(xiàn)接口interface的問題
這篇文章主要介紹了Go 通過結(jié)構(gòu)struct實現(xiàn)接口interface的問題,本文通過示例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-10-10