Go語(yǔ)言中節(jié)省內(nèi)存技巧方法示例
引言
GO雖然不消耗大量?jī)?nèi)存,但是仍有一些小技巧可以節(jié)省內(nèi)存,良好的編碼習(xí)慣是每一個(gè)程序員都應(yīng)該具備的素質(zhì)。
預(yù)先分配切片
數(shù)組是具有連續(xù)內(nèi)存的相同類型的集合。數(shù)組類型定義時(shí)要指定長(zhǎng)度和元素類型。
因?yàn)閿?shù)組的長(zhǎng)度是它們類型的一部分,數(shù)組的主要問(wèn)題是它們大小固定,不能調(diào)整。
與數(shù)組類型不同,切片類型無(wú)需指定長(zhǎng)度。切片的聲明方式與數(shù)組相同,但沒(méi)有數(shù)量元素。
切片是數(shù)組的包裝器,它們不擁有任何數(shù)據(jù)——它們是對(duì)數(shù)組的引用。它們由指向數(shù)組的指針、長(zhǎng)度及其容量(底層數(shù)組中的元素?cái)?shù))組成。
當(dāng)您向沒(méi)有足夠容量的切片添加一個(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
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的時(shí)候,是不給它分配內(nèi)存的,內(nèi)存地址為0,大小和容量也都是0 后面每次擴(kuò)容都是2的倍數(shù),并且每次擴(kuò)容內(nèi)存地址都發(fā)生了改變。
當(dāng)容量<1024 時(shí)會(huì)漲為之前的 2 倍,當(dāng)容量>=1024時(shí)會(huì)以 1.25 倍增長(zhǎng)。從 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 個(gè)字節(jié)是來(lái)自哪里呢?
現(xiàn)代 64 位 CPU 以 64 位(8 字節(jié))的塊獲取數(shù)據(jù)
第一個(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),在此過(guò)程中節(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))
}
上述的輸出為 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
}
第一個(gè)結(jié)構(gòu)體占用128字節(jié),第二個(gè)結(jié)構(gòu)體占用72字節(jié)。節(jié)省空間:(128-72)/129=43.75%.
在 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é)
使用 map[string]struct{} 而不是 map[string]bool
Go 沒(méi)有內(nèi)置的集合,通常使用 map[string]bool{} 表示集合。盡管它更具可讀性(這非常重要),但將其作為一個(gè)集合使用是錯(cuò)誤的,因?yàn)樗哂袃煞N狀態(tài)(假/真)并且與空結(jié)構(gòu)體相比使用了額外的內(nèi)存。
空結(jié)構(gòu)體 (struct{}) 是沒(méi)有額外字段的結(jié)構(gòu)類型,占用零字節(jié)的存儲(chǔ)空間。
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值有個(gè)好處是查找的時(shí)候更方便,從map中取值只需要判斷一個(gè)值就行了,而使用空結(jié)構(gòu)體則需要判斷第二個(gè)值
m := make(map[string]bool{})
if m["key"]{
// Do something
}
v := make(map[string]struct{}{})
if _, ok := v["key"]; ok{
// Do something
}
參考
【1】Go 中簡(jiǎn)單的內(nèi)存節(jié)省技巧
【2】Easy memory-saving tricks in Go
以上就是Go語(yǔ)言中節(jié)省內(nèi)存技巧方法示例的詳細(xì)內(nèi)容,更多關(guān)于Go語(yǔ)言節(jié)省內(nèi)存技巧的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 提升Go語(yǔ)言開發(fā)效率的小技巧實(shí)例(GO語(yǔ)言語(yǔ)法糖)匯總
- 學(xué)會(huì)提升Go語(yǔ)言編碼效率技巧拒絕加班!
- go?mongox簡(jiǎn)潔高效文檔操作及bson數(shù)據(jù)構(gòu)造流暢技巧
- 從錯(cuò)誤中學(xué)習(xí)改正Go語(yǔ)言六個(gè)壞習(xí)慣提高編程技巧
- GoJs中標(biāo)題和縮略圖使用技巧
- Go內(nèi)存節(jié)省技巧簡(jiǎn)單實(shí)現(xiàn)方法
- go語(yǔ)言優(yōu)雅地處理error工具及技巧詳解
- Go語(yǔ)言開發(fā)技巧必知的小細(xì)節(jié)提升效率
相關(guān)文章
Go1.18?新特性之多模塊Multi-Module工作區(qū)模式
這篇文章主要介紹了Go1.18?新特性之多模塊Multi-Module工作區(qū)模式,在 Go 1.18之前,建議使用依賴模塊中的 replace 指令來(lái)處理這個(gè)問(wèn)題,從 Go 1.18開始引入了一種同時(shí)處理多個(gè)模塊的新方法,通過(guò)案例給大家詳細(xì)介紹,感興趣的朋友一起看看吧2022-04-04
使用Go語(yǔ)言實(shí)現(xiàn)一個(gè)簡(jiǎn)單的無(wú)界資源池
本文我們希望通過(guò)go語(yǔ)言實(shí)現(xiàn)一個(gè)簡(jiǎn)單的資源池,而這個(gè)資源池的資源包括但不限于數(shù)據(jù)庫(kù)連接池,線程池,協(xié)程池,網(wǎng)絡(luò)連接池,只要這些資源實(shí)現(xiàn)我們指定的關(guān)閉方法,則都可以通過(guò)我們封裝的資源池進(jìn)行統(tǒng)一管理,文中通過(guò)代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-05-05
go語(yǔ)言實(shí)現(xiàn)Elasticsearches批量修改查詢及發(fā)送MQ操作示例
這篇文章主要為大家介紹了go語(yǔ)言實(shí)現(xiàn)Elasticsearches批量修改查詢及發(fā)送MQ操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
Go語(yǔ)言實(shí)現(xiàn)單端口轉(zhuǎn)發(fā)到多個(gè)端口
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言實(shí)現(xiàn)單端口轉(zhuǎn)發(fā)到多個(gè)端口,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的小伙伴可以了解下2024-02-02
詳解Go語(yǔ)言中獲取文件路徑的不同方法與應(yīng)用場(chǎng)景
在使用?Go?開發(fā)項(xiàng)目時(shí),估計(jì)有不少人遇到過(guò)無(wú)法正確處理文件路徑的問(wèn)題,本文將嘗試從簡(jiǎn)單到復(fù)雜,詳細(xì)介紹?Go?中獲取路徑的不同方法及應(yīng)用場(chǎng)景,希望對(duì)大家有所幫助2024-02-02
golang 實(shí)現(xiàn)一個(gè)負(fù)載均衡案例(隨機(jī),輪訓(xùn))
這篇文章主要介紹了golang 實(shí)現(xiàn)一個(gè)負(fù)載均衡案例(隨機(jī)、輪訓(xùn)),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
Go 通過(guò)結(jié)構(gòu)struct實(shí)現(xiàn)接口interface的問(wèn)題
這篇文章主要介紹了Go 通過(guò)結(jié)構(gòu)struct實(shí)現(xiàn)接口interface的問(wèn)題,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10

