網站首頁 編程語言 正文
引言
除非您正在對服務進行原型設計,否則您可能會關心應用程序的內存使用情況。內存占用更小,基礎設施成本降低,擴展變得更容易/延遲。
盡管 Go 以不消耗大量內存而聞名,但仍有一些方法可以進一步減少消耗。其中一些需要大量重構,但很多都很容易做到。
預先分配切片
數組是具有連續內存的相同類型的集合。數組類型定義指定長度和元素類型。數組的主要問題是它們的大小是固定的——它們不能調整大小,因為數組的長度是它們類型的一部分。
與數組類型不同,切片類型沒有指定長度。切片的聲明方式與數組相同,但沒有元素計數。
切片是數組的包裝器,它們不擁有任何數據——它們是對數組的引用。它們由指向數組的指針、段的長度及其容量(底層數組中的元素數)組成。
當您追加到一個沒有新值容量的切片時 - 會創建一個具有更大容量的新數組,并將當前數組中的值復制到新數組中。這會導致不必要的分配和 CPU 周期。
為了更好地理解這一點,讓我們看一下以下代碼段:
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]
查看輸出,我們可以得出結論,無論何時必須增加容量(增加 2 倍),都必須創建一個新的底層數組(新的內存地址)并將值復制到新數組中。
有趣的事實是,容量增長的因素曾經是容量 <1024 的 2 倍,以及 >= 1024 的 1.25 倍。從 Go 1.18 開始,這已經變得更加線性。
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
查看上述基準,我們可以得出結論,將值分配給預分配的切片和將值附加到切片之間存在很大差異。
兩個 linter 有助于預分配切片:
- prealloc: 一種靜態分析工具,用于查找可能被預分配的切片聲明。
- makezero: 一種靜態分析工具,用于查找未以零長度初始化且稍后與 append 一起使用的切片聲明。
結構中的順序字段
您之前可能沒有想到這一點,但結構中字段的順序對內存消耗很重要。
以下面的結構為例:
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 字節)塊的形式獲取數據。如果我們有一個較舊的 32 位 CPU,它將執行 32 位(4 字節)的塊。
第一個周期占用 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 IsDeleted bool // 1 byte } func main(){ p := Post{} fmt.Println(unsafe.Sizeof(p)) }
在 64 位架構上占用 <8 字節的 Go 類型:
- bool:1 個字節
- int8/uint8:1 個字節
- int16/uint16:2 個字節
- int32/uint32/rune:4 字節
- float32:4 字節
- byte:1個字節
無需手動檢查結構并按大小對其進行排序,而是使用 linter 找到這些結構并(用于)報告“正確”排序。
- maligned: 不推薦使用的 linter,用于報告未對齊的結構并打印出正確排序的字段。它在一年前被棄用,但您仍然可以安裝舊版本并使用它。
- govet/fieldalignment: 作為 gotools 和 govet linter 的一部分,fieldalignment 打印出未對齊的結構和結構的當前/理想大小。
要安裝和運行 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 沒有內置集合,通常使用 map[string]bool{}
來表示集合。盡管它更具可讀性,這一點非常重要,但將其作為一個集合使用是錯誤的,因為它有兩種狀態(假/真),并且與空結構相比使用了額外的內存。
空結構體 (struct{}
) 是沒有額外字段的結構體類型,占用零字節存儲空間。
我不建議這樣做,除非您的 map/set 包含大量值并且您需要獲得額外的內存或者您正在為低內存平臺進行開發。
使用 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{}{} } }
得到以下結果,在整個運行過程中非常一致:
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%
使用這些數字,我們可以得出結論,使用空結構映射的寫入速度提高了 3.2%,分配的內存減少了 10%。
此外,使用 map[type]struct{}
是實現集合的正確解決方法,因為每個鍵都有一個值。使用 map[type]bool
,每個鍵都有兩個可能的值,這不是一個集合,如果目標是創建一個集合,則可能會被誤用。
然而,可讀性大多數時候比(可忽略的)內存改進更重要。與空結構體相比,使用布爾值更容易掌握查找:
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)
原文鏈接:https://juejin.cn/post/7133984343881416741
相關推薦
- 2022-07-02 pyscript的簡單應用實現_python
- 2022-12-02 python3中join和格式化的用法小結_python
- 2022-07-16 Spring MVC重定向和轉發
- 2022-04-07 對WPF中Expander控件美化_實用技巧
- 2022-04-17 實時檢測文件夾變化,及時同步文件到服務器python 腳本
- 2022-06-16 python遺傳算法之單/多目標規劃問題_python
- 2023-06-03 C#利用后綴表達式解析計算字符串公式_C#教程
- 2022-10-17 Kotlin編程基礎語法編碼規范_Golang
- 最近更新
-
- 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同步修改后的遠程分支