欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang內(nèi)存管理之內(nèi)存逃逸分析

 更新時(shí)間:2023年07月04日 08:26:28   作者:IguoChan  
逃逸分析是指由編譯器決定內(nèi)存分配的位置,不需要程序員指定,這篇文章主要為大家詳細(xì)介紹了Golang中內(nèi)存逃逸分析的幾種方法,需要的可以參考一下

0. 簡介

前面我們針對(duì)Go中堆和棧的內(nèi)存都做了一些分析,現(xiàn)在我們來分析一下Go的內(nèi)存逃逸。

學(xué)習(xí)過C語言的都知道,在C棧區(qū)域會(huì)存放函數(shù)的參數(shù)、局部變量等,而這些局部變量的地址是不能返回的,除非是局部靜態(tài)變量地址,字符串常量地址或者動(dòng)態(tài)分配的地址,因?yàn)槌绦蛘{(diào)用完函數(shù)后,局部變量會(huì)隨著此函數(shù)的棧幀一起被釋放。而對(duì)于程序員主動(dòng)申請(qǐng)的內(nèi)存則存儲(chǔ)在堆上,需要使用malloc等函數(shù)進(jìn)行申請(qǐng),同時(shí)也需要使用free等函數(shù)釋放,由程序員進(jìn)行管理,而申請(qǐng)內(nèi)存后如果沒有釋放,就有可能造成內(nèi)存泄漏。

但是在Go中,程序員根本無需感知數(shù)據(jù)是在棧(Go棧)上,還是在堆上,因?yàn)榫幾g器會(huì)幫你承擔(dān)這一切,將內(nèi)存分配到?;蛘叨焉?。在編譯器優(yōu)化中,逃逸分析是用來決定指針動(dòng)態(tài)作用域的方法。Go語言的編譯器使用逃逸分析決定哪些變量應(yīng)該分配在棧上,哪些變量應(yīng)該分配在堆上,包括使用new、make和字面量等方式隱式分配的內(nèi)存,Go語言逃逸分析遵循以下兩個(gè)不變性:

  • 指向棧對(duì)象的指針不能存在于堆中;
  • 指向棧對(duì)象的指針不能在棧對(duì)象回收后存活;

逃逸分析是在編譯階段進(jìn)行的,可以通過go build -gcflags="-m -m -l"命令查到逃逸分析的結(jié)果,最多可以提供4個(gè)-m, m 越多則表示分析的程度越詳細(xì),一般情況下我們可以采用兩個(gè)-m分析。使用-l禁用掉內(nèi)聯(lián)優(yōu)化,只關(guān)注逃逸優(yōu)化即可。

1. 幾種逃逸分析

1.1 函數(shù)返回局部變量指針

package main
func Add(x, y int) *int {
   res := 0
   res = x + y
   return &res
}
func main() {
   Add(1, 2)
}

逃逸分析結(jié)果如下:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:4:2: res escapes to heap:
./main.go:4:2:   flow: ~r2 = &res:
./main.go:4:2:     from &res (address-of) at ./main.go:6:9
./main.go:4:2:     from return &res (return) at ./main.go:6:2
./main.go:4:2: moved to heap: res
note: module requires Go 1.18

分析結(jié)果很明顯,函數(shù)返回的局部變量是一個(gè)指針變量,當(dāng)函數(shù)Add執(zhí)行結(jié)束后,對(duì)應(yīng)的棧幀就會(huì)被銷毀,引用返回到函數(shù)之外,如果在外部解引用這個(gè)地址,就會(huì)導(dǎo)致程序訪問非法內(nèi)存,所以編譯器會(huì)經(jīng)過逃逸分析后在堆上分配內(nèi)存。

1.2 interface(any)類型逃逸

package main
import (
   "fmt"
)
func main() {
   str := "hello world"
   fmt.Printf("%v\n", str)
}

逃逸分析如下:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:9:13: str escapes to heap:
./main.go:9:13:   flow: {storage for ... argument} = &{storage for str}:
./main.go:9:13:     from str (spill) at ./main.go:9:13
./main.go:9:13:     from ... argument (slice-literal-element) at ./main.go:9:12
./main.go:9:13:   flow: {heap} = {storage for ... argument}:
./main.go:9:13:     from ... argument (spill) at ./main.go:9:12
./main.go:9:13:     from fmt.Printf("%v\n", ... argument...) (call parameter) at ./main.go:9:12
./main.go:9:12: ... argument does not escape
./main.go:9:13: str escapes to heap

通過這個(gè)分析你可能會(huì)認(rèn)為str escapes to heap表示這個(gè)str逃逸到了堆,但是卻沒有上一節(jié)中返回值中明確寫上moved to heap: res,那實(shí)際上str是否真的逃逸到了堆上呢?

escapes to heap vs moved to heap

我們可以寫如下代碼試試:

package main
import "fmt"
func main() {
   str := "hello world"
   str1 := "nihao!"
   fmt.Printf("%s\n", str)
   println(&str)
   println(&str1)
}

其逃逸分析和上面的沒有區(qū)別:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:8:13: str escapes to heap:
./main.go:8:13:   flow: {storage for ... argument} = &{storage for str}:
./main.go:8:13:     from str (spill) at ./main.go:8:13
./main.go:8:13:     from ... argument (slice-literal-element) at ./main.go:8:12
./main.go:8:13:   flow: {heap} = {storage for ... argument}:
./main.go:8:13:     from ... argument (spill) at ./main.go:8:12
./main.go:8:13:     from fmt.Printf("%s\n", ... argument...) (call parameter) at ./main.go:8:12
./main.go:8:12: ... argument does not escape
./main.go:8:13: str escapes to heap
note: module requires Go 1.18

但是,str1str二者的地址卻是明顯相鄰的,那是怎么回事呢?

$ go run main.go
hello world
0xc00009af50
0xc00009af40

如果我們將上述代碼的第8行fmt.Printf("%s\n", str)改為fmt.Printf("%p\n", &str),則逃逸分析如下,發(fā)現(xiàn)多了一行moved to heap: str

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:6:2: str escapes to heap:
./main.go:6:2:   flow: {storage for ... argument} = &str:
./main.go:6:2:     from &str (address-of) at ./main.go:8:21
./main.go:6:2:     from &str (interface-converted) at ./main.go:8:21
./main.go:6:2:     from ... argument (slice-literal-element) at ./main.go:8:12
./main.go:6:2:   flow: {heap} = {storage for ... argument}:
./main.go:6:2:     from ... argument (spill) at ./main.go:8:12
./main.go:6:2:     from fmt.Printf("%p\n", ... argument...) (call parameter) at ./main.go:8:12
./main.go:6:2: moved to heap: str
./main.go:8:12: ... argument does not escape
note: module requires Go 1.18

再看運(yùn)行結(jié)果,發(fā)現(xiàn)看起來str的地址看起來像逃逸到了堆,畢竟和str1的地址明顯不同:

$ go run main.go
0xc00010a210
0xc00010a210
0xc000106f50

參考如下解釋

When the escape analysis says "b escapes to heap", it means that the values in b are written to the heap. So anything referenced by b must be in the heap also. b itself need not be.

翻譯過來大意是:當(dāng)逃逸分析輸出“b escapes to heap”時(shí),意思是指存儲(chǔ)在b中的值逃逸到堆上了,即任何被b引用的對(duì)象必須分配在堆上,而b自身則不需要;如果b自身也逃逸到堆上,那么逃逸分析會(huì)輸出“&b escapes to heap”。

由于字符串本身是存儲(chǔ)在只讀存儲(chǔ)區(qū),我們使用切片更能表現(xiàn)以上的特性。

無逃逸

package main
import (
   "reflect"
   "unsafe"
)
func main() {
   var i int
   i = 10
   println("&i", &i)
   b := []int{1, 2, 3, 4, 5}
   println("&b", &b) // b這個(gè)對(duì)象的地址
   println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數(shù)組地址
}

逃逸分析是:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:12:12: []int{...} does not escape
note: module requires Go 1.18

打印結(jié)果:

$ go run main.go
&i 0xc00009af20
&b 0xc00009af58
b 0xc00009af28

可以看到,以上分析無逃逸,且&i b &b地址連續(xù),可以明顯看到都在棧中。

切片底層數(shù)組逃逸

我們新增一個(gè)fmt包的打?。?/p>

package main
import (
   "fmt"
   "reflect"
   "unsafe"
)
func main() {
   var i int
   i = 10
   println("&i", &i)
   b := []int{1, 2, 3, 4, 5}
   println("&b", &b) // b這個(gè)對(duì)象的地址
   println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數(shù)組地址
   fmt.Println(b) // 多加了這行
}

逃逸分析如下:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:16:13: b escapes to heap:
./main.go:16:13:   flow: {storage for ... argument} = &{storage for b}:
./main.go:16:13:     from b (spill) at ./main.go:16:13
./main.go:16:13:     from ... argument (slice-literal-element) at ./main.go:16:13
./main.go:16:13:   flow: {heap} = {storage for ... argument}:
./main.go:16:13:     from ... argument (spill) at ./main.go:16:13
./main.go:16:13:     from fmt.Println(... argument...) (call parameter) at ./main.go:16:13
./main.go:13:12: []int{...} escapes to heap:
./main.go:13:12:   flow: b = &{storage for []int{...}}:
./main.go:13:12:     from []int{...} (spill) at ./main.go:13:12
./main.go:13:12:     from b := []int{...} (assign) at ./main.go:13:4
./main.go:13:12:   flow: {storage for b} = b:
./main.go:13:12:     from b (interface-converted) at ./main.go:16:13
./main.go:13:12: []int{...} escapes to heap
./main.go:16:13: ... argument does not escape
./main.go:16:13: b escapes to heap
note: module requires Go 1.18

可以發(fā)現(xiàn),出現(xiàn)了b escapes to heap,然后查看打印:

$ go run main.go
&i 0xc000106f38
&b 0xc000106f58
b 0xc000120030
[1 2 3 4 5]

可以發(fā)現(xiàn),b的底層數(shù)組發(fā)生了逃逸,但是b本身還是在棧中。

切片對(duì)象同樣發(fā)生逃逸

package main
import (
   "fmt"
   "reflect"
   "unsafe"
)
func main() {
   var i int
   i = 10
   println("&i", &i)
   b := []int{1, 2, 3, 4, 5}
   println("&b", &b) // b這個(gè)對(duì)象的地址
   println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)) // b的底層數(shù)組地址
   fmt.Println(&b) // 修改這行
}

如上,將fmt.Println(b)改為fmt.Println(&b),逃逸分析如下:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:13:2: b escapes to heap:
./main.go:13:2:   flow: {storage for ... argument} = &b:
./main.go:13:2:     from &b (address-of) at ./main.go:16:14
./main.go:13:2:     from &b (interface-converted) at ./main.go:16:14
./main.go:13:2:     from ... argument (slice-literal-element) at ./main.go:16:13
./main.go:13:2:   flow: {heap} = {storage for ... argument}:
./main.go:13:2:     from ... argument (spill) at ./main.go:16:13
./main.go:13:2:     from fmt.Println(... argument...) (call parameter) at ./main.go:16:13
./main.go:13:12: []int{...} escapes to heap:
./main.go:13:12:   flow: b = &{storage for []int{...}}:
./main.go:13:12:     from []int{...} (spill) at ./main.go:13:12
./main.go:13:12:     from b := []int{...} (assign) at ./main.go:13:4
./main.go:13:2: moved to heap: b
./main.go:13:12: []int{...} escapes to heap
./main.go:16:13: ... argument does not escape
note: module requires Go 1.18

發(fā)現(xiàn)多了moved to heap: b這行,然后看地址打?。?/p>

$ go run main.go
&i 0xc00006af48
&b 0xc00000c030
b 0xc00001a150
&[1 2 3 4 5]

發(fā)現(xiàn)不僅底層數(shù)組發(fā)生了逃逸,連b這個(gè)對(duì)象本身也發(fā)生了逃逸。

所以可以總結(jié)下來就是:

  • escapes to heap:表示這個(gè)對(duì)象里面的指針對(duì)象逃逸到堆中;
  • moved to heap:表示對(duì)象本身逃逸到堆中,根據(jù)指向棧對(duì)象的指針不能存在于堆中這一準(zhǔn)則,該對(duì)象里面的指針對(duì)象特必然逃逸到堆中。

1.3 申請(qǐng)??臻g過大

package main
import (
   "reflect"
   "unsafe"
)
func main() {
   var i int
   i = 10
   println("&i", &i)
   b := make([]int, 0)
   println("&b", &b) // b這個(gè)對(duì)象的地址
   println("b", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b)).Data))
   b1 := make([]byte, 65536)
   println("&b1", &b1) // b1這個(gè)對(duì)象的地址
   println("b1", unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&b1)).Data))
   var a [1024*1024*10]byte
   _ = a
}

可以發(fā)現(xiàn)逃逸分析顯示沒有發(fā)生逃逸:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:13:11: make([]int, 0) does not escape
./main.go:17:12: make([]byte, 65536) does not escape
note: module requires Go 1.18

如果將切片和數(shù)組的長度都增加1,則會(huì)發(fā)生逃逸。

b1 := make([]byte, 65537)
var a [1024*1024*10 + 1]byte

逃逸分析:

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:21:6: a escapes to heap:
./main.go:21:6:   flow: {heap} = &a:
./main.go:21:6:     from a (too large for stack) at ./main.go:21:6
./main.go:17:12: make([]byte, 65537) escapes to heap:
./main.go:17:12:   flow: {heap} = &{storage for make([]byte, 65537)}:
./main.go:17:12:     from make([]byte, 65537) (too large for stack) at ./main.go:17:12
./main.go:21:6: moved to heap: a
./main.go:13:11: make([]int, 0) does not escape
./main.go:17:12: make([]byte, 65537) escapes to heap
note: module requires Go 1.18

可以發(fā)現(xiàn)切片類型的逃逸閾值是65536 = 64KB,數(shù)組類型的逃逸閾值是1024*1024*10 = 10MB,超過這個(gè)數(shù)值就會(huì)發(fā)生逃逸。

1.4 閉包逃逸

package main
func intSeq() func() int {
   i := 0
   return func() int {
      i++
      return i
   }
}
func main() {
    a := intSeq()
    println(a())
    println(a())
    println(a())
    println(a())
    println(a())
    println(a())
}

逃逸分析如下,可以發(fā)現(xiàn)閉包中的局部變量i發(fā)生了逃逸。

$ go build -gcflags="-m -m -l" ./main.go
# command-line-arguments
./main.go:4:2: intSeq capturing by ref: i (addr=false assign=true width=8)
./main.go:5:9: func literal escapes to heap:
./main.go:5:9:   flow: ~r0 = &{storage for func literal}:
./main.go:5:9:     from func literal (spill) at ./main.go:5:9
./main.go:5:9:     from return func literal (return) at ./main.go:5:2
./main.go:4:2: i escapes to heap:
./main.go:4:2:   flow: {storage for func literal} = &i:
./main.go:4:2:     from i (captured by a closure) at ./main.go:6:3
./main.go:4:2:     from i (reference) at ./main.go:6:3
./main.go:4:2: moved to heap: i
./main.go:5:9: func literal escapes to heap
note: module requires Go 1.18

因?yàn)楹瘮?shù)也是一個(gè)指針類型,所以匿名函數(shù)當(dāng)作返回值時(shí)也發(fā)生了逃逸,在匿名函數(shù)中使用外部變量i,這個(gè)變量i會(huì)一直存在直到a被銷毀,所以i變量逃逸到了堆上。

2. 總結(jié)

逃逸到堆上的內(nèi)存可能會(huì)加大GC壓力,所以在一些簡單的場(chǎng)景下,我們可以避免內(nèi)存逃逸,使得變量更多地分配在棧上,可以提升程序的性能。比如:

  • 不要盲目地使用指針傳參,特別是參數(shù)對(duì)象很小時(shí),雖然可以減小復(fù)制大小,但是可能會(huì)造成內(nèi)存逃逸;
  • 多根據(jù)代碼具體分析,根據(jù)逃逸分析結(jié)果做一些優(yōu)化,提高性能。

以上就是Golang內(nèi)存管理之內(nèi)存逃逸分析的詳細(xì)內(nèi)容,更多關(guān)于Golang內(nèi)存逃逸的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go語言中的通道chan使用指南

    Go語言中的通道chan使用指南

    Go語言的通道chan是實(shí)現(xiàn)并發(fā)編程的關(guān)鍵工具,主要用于goroutine之間的數(shù)據(jù)傳輸,本文主要介紹了通道的基本操作如創(chuàng)建、發(fā)送、接收和關(guān)閉數(shù)據(jù),以及使用select語句進(jìn)行多路復(fù)用和超時(shí)控制,感興趣的可以了解一下
    2024-10-10
  • golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫功能

    golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫功能

    這篇文章主要為大家詳細(xì)介紹了golang實(shí)現(xiàn)文件上傳并轉(zhuǎn)存數(shù)據(jù)庫功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-07-07
  • 詳解Golang語言中的interface

    詳解Golang語言中的interface

    這篇文章主要介紹了Golang語言中的interface的相關(guān)資料,幫助大家更好的理解和使用golang,感興趣的朋友可以了解下
    2021-01-01
  • Golang使用Consul詳解

    Golang使用Consul詳解

    Consul是一個(gè)服務(wù)發(fā)現(xiàn)軟件, 提供了服務(wù)發(fā)現(xiàn)\鍵值存儲(chǔ)\健康檢查等功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Go?中的空白標(biāo)識(shí)符下劃線

    Go?中的空白標(biāo)識(shí)符下劃線

    這篇文章主要介紹了Go?中的空白標(biāo)識(shí)符下劃線,空白標(biāo)識(shí)符是未使用的值的占位符,由下劃線(_)表示,下文對(duì)其相關(guān)介紹需要的小伙伴可以參考一下
    2022-03-03
  • 使用Golang實(shí)現(xiàn)流式輸出

    使用Golang實(shí)現(xiàn)流式輸出

    這篇文章主要為大家詳細(xì)介紹了使用Golang實(shí)現(xiàn)流式輸出的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-03-03
  • Go微服務(wù)網(wǎng)關(guān)的實(shí)現(xiàn)

    Go微服務(wù)網(wǎng)關(guān)的實(shí)現(xiàn)

    本文主要介紹了Go微服務(wù)網(wǎng)關(guān)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Golang時(shí)間及時(shí)間戳的獲取轉(zhuǎn)換超全面詳細(xì)講解

    Golang時(shí)間及時(shí)間戳的獲取轉(zhuǎn)換超全面詳細(xì)講解

    說實(shí)話,golang的時(shí)間轉(zhuǎn)化還是很麻煩的,最起碼比php麻煩很多,下面這篇文章主要給大家介紹了關(guān)于golang時(shí)間/時(shí)間戳的獲取與轉(zhuǎn)換的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-12-12
  • golang time包做時(shí)間轉(zhuǎn)換操作

    golang time包做時(shí)間轉(zhuǎn)換操作

    這篇文章主要介紹了golang time包做時(shí)間轉(zhuǎn)換操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • golang中單機(jī)鎖的具體實(shí)現(xiàn)詳解

    golang中單機(jī)鎖的具體實(shí)現(xiàn)詳解

    這篇文章主要為大家詳細(xì)介紹了golang中單機(jī)鎖的具體實(shí)現(xiàn)的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-03-03

最新評(píng)論