日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Go語言中節省內存技巧方法示例_Golang

作者:nil ? 更新時間: 2023-02-15 編程語言

引言

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

欄目分類
最近更新