網站首頁 編程語言 正文
引言
GO雖然不消耗大量內存,但是仍有一些小技巧可以節省內存,良好的編碼習慣是每一個程序員都應該具備的素質。
預先分配切片
數組是具有連續內存的相同類型的集合。數組類型定義時要指定長度和元素類型。
因為數組的長度是它們類型的一部分,數組的主要問題是它們大小固定,不能調整。
與數組類型不同,切片類型無需指定長度。切片的聲明方式與數組相同,但沒有數量元素。
切片是數組的包裝器,它們不擁有任何數據——它們是對數組的引用。它們由指向數組的指針、長度及其容量(底層數組中的元素數)組成。
當您向沒有足夠容量的切片添加一個新值時 - 會創建一個具有更大容量的新數組,并將當前數組中的值復制到新數組中。這會導致不必要的內存分配和 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) } }
結果
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]
可以看到第一次聲明數組var ints []int
的時候,是不給它分配內存的,內存地址為0,大小和容量也都是0 后面每次擴容都是2的倍數,并且每次擴容內存地址都發生了改變。
當容量<1024 時會漲為之前的 2 倍,當容量>=1024時會以 1.25 倍增長。從 Go 1.18 開始,這已經變得更加線性
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) } }
結果如下
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
由上述基準,我們可以得出結論,將值分配給預分配的切片和將值追加到切片之間是存在很大差異的。預先分配大小可以提速3倍多,而且內存分配也更小。
結構體中的字段順序
以下面結構體為例
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 字節,而所有字段相加為 82 字節。那額外的 14 個字節是來自哪里呢?
現代 64 位 CPU 以 64 位(8 字節)的塊獲取數據
第一個周期占用 8 個字節,拉取“IsDraft”字段占用了 1 個字節并且產生 7 個未使用字節。它不能占用“一半”的字段。
第二個和第三個周期取 Title 字符串,第四個周期取 ID,依此類推。到取 IsDeleted 字段時,它使用 1 個字節并有 7 個字節未使用。
對內存節省的關鍵是按字段占用大小從上到下對字段進行排序。對上述結構進行排序,大小可減少到 88 個字節。最后兩個字段 IsDraft 和 IsDeleted 被放在同一個塊中,從而將未使用的字節數從 14 (2x7) 減少到 6 (1 x 6),在此過程中節省了 8 個字節。
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 字節
極端情況
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 }
第一個結構體占用128字節,第二個結構體占用72字節。節省空間:(128-72)/129=43.75%.
在 64 位架構上占用小于 8 字節的 Go 類型:
- bool: 1 個字節
- int8/uint8: 1 個字節
- int16/uint16: 2 個字節
- int32/uint32/rune: 4 個字節
- float32: 4 個字節
- byte: 1 個字節
使用 map[string]struct{} 而不是 map[string]bool
Go 沒有內置的集合,通常使用?map[string]bool{}
?表示集合。盡管它更具可讀性(這非常重要),但將其作為一個集合使用是錯誤的,因為它具有兩種狀態(假/真)并且與空結構體相比使用了額外的內存。
空結構體 (struct{}
) 是沒有額外字段的結構類型,占用零字節的存儲空間。
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{}{} } }
結果
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
可以看到執行速度提升了一些,但是效果不太明顯。
使用bool值有個好處是查找的時候更方便,從map中取值只需要判斷一個值就行了,而使用空結構體則需要判斷第二個值
m := make(map[string]bool{}) if m["key"]{ // Do something } v := make(map[string]struct{}{}) if _, ok := v["key"]; ok{ // Do something }
參考
【1】Go 中簡單的內存節省技巧
【2】Easy memory-saving tricks in Go
原文鏈接:https://juejin.cn/post/7143993168596303879
相關推薦
- 2022-08-15 詳解Redis分布式鎖的原理與實現_Redis
- 2022-09-03 Golang棧結構和后綴表達式實現計算器示例_Golang
- 2022-07-08 C#中的GDI+圖像編程詳解_C#教程
- 2023-05-29 Python中如何給字典設置默認值_python
- 2022-11-20 Pandas數據處理庫畫圖與文件讀取使用示例_python
- 2022-03-21 詳解C語言動態內存的分配_C 語言
- 2022-05-22 PO模式在selenium自動化測試框架的優勢_python
- 2022-04-19 npm install運行原理分析
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支