偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Go語(yǔ)言內(nèi)存逃逸之謎

開發(fā) 后端 存儲(chǔ)軟件
所謂逃逸分析就是在編譯階段由編譯器根據(jù)變量的類型、外部使用情況等因素來(lái)判定是分配到堆還是棧,從而替代人工處理。

[[402433]]

本文轉(zhuǎn)載自微信公眾號(hào)「后端技術(shù)指南針」,作者大白。轉(zhuǎn)載本文請(qǐng)聯(lián)系后端技術(shù)指南針公眾號(hào)。

我們?cè)诟咧袑W(xué)過一些天體物理的知識(shí),比如常見的三個(gè)宇宙速度:

  • 第一宇宙速度:航天器逃離地面圍繞地球做圓周運(yùn)動(dòng)的最小速度:7.9km/s
  • 第二宇宙速度:航天器逃離地球的最小速度:11.18km/s
  • 第三宇宙速度:航天器逃離太陽(yáng)系的最小速度:16.64km/s

了解了航天器的逃逸行為,我們今天來(lái)點(diǎn)特別的:內(nèi)存逃逸。

通過本文你將了解到以下內(nèi)容:

  • C/C++的內(nèi)存布局和堆棧
  • Go的內(nèi)存逃逸和逃逸分析
  • 內(nèi)存逃逸的小結(jié)

Part1C/C++的內(nèi)存布局和堆棧

這應(yīng)該是一道出現(xiàn)頻率極高的面試題。

C/C++作為靜態(tài)強(qiáng)類型語(yǔ)言,編譯成二進(jìn)制文件后,運(yùn)行時(shí)整個(gè)程序的內(nèi)存空間分為:

  • 內(nèi)核空間 Kernel Space
  • 用戶空間 User Space

內(nèi)核空間主要存放進(jìn)程運(yùn)行時(shí)的一些控制信息,用戶空間則是存放程序本身的一些信息,我們來(lái)看下用戶空間的布局:

堆和棧的主要特點(diǎn):

  • 棧區(qū)(Stack):由編譯器自動(dòng)分配釋放,存儲(chǔ)函數(shù)的參數(shù)值,局部變量值等,但是空間一般較小數(shù)KB~數(shù)MB
  • 堆區(qū)(Heap):C/C++沒有GC機(jī)制,堆內(nèi)存一般由程序員申請(qǐng)和釋放,空間較大,能否用好取決于使用者的水平

Go語(yǔ)言與C語(yǔ)言淵源極深,C語(yǔ)言面臨的問題,Go同樣會(huì)面對(duì),比如:變量的內(nèi)存分配問題。

  • 在C語(yǔ)言中,需要程序員自己根據(jù)需要來(lái)確定采用堆還是棧,棧內(nèi)存由OS全權(quán)負(fù)責(zé),但是堆內(nèi)存需要顯式調(diào)用malloc/new等函數(shù)申請(qǐng),并且對(duì)應(yīng)調(diào)用free/delete來(lái)釋放。
  • Go語(yǔ)言具有垃圾回收Garbage Collection機(jī)制來(lái)進(jìn)行堆內(nèi)存管理,并且沒有像malloc/new這種堆內(nèi)存分配的關(guān)鍵字。
  • 棧內(nèi)存的分配和釋放開銷非常小,堆內(nèi)存對(duì)于Go來(lái)說(shuō)開銷比棧內(nèi)存大很多。

Part2Go的內(nèi)存逃逸和逃逸分析

如果寫過C/C++都會(huì)知道,在函數(shù)內(nèi)部聲明局部變量,然后返回其指針,如果外部調(diào)用則會(huì)報(bào)錯(cuò):

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. int* getValue() 
  5.  int val = 10086; 
  6.  return &val; 
  7.  
  8. int main() 
  9.    cout<<*getValue()<<endl; 
  10.    return 0; 

編譯上述代碼:main.cpp: In function ‘int* getValue()’: main.cpp:7:9: warning: address of local variable ‘val’ returned [-Wreturn-local-addr]

用同樣的思想,寫一個(gè)go版本的代碼:

  1. package main 
  2.  
  3. import ( 
  4.  "fmt" 
  5.  
  6. func main() { 
  7.     str := GetString() 
  8.     fmt.Println(*str) 
  9.  
  10. func GetString() *string { 
  11.     var s string 
  12.     s = "hello world" 
  13.     return &s 

代碼卻可以正常運(yùn)行,我們本意是在棧上分配一個(gè)變量,用完就銷毀,但是外部卻調(diào)用了,甚至可以正常進(jìn)行,表現(xiàn)和C++完全不同。

其實(shí),這就是Go的內(nèi)存逃逸現(xiàn)象,Go模糊了棧內(nèi)存和堆內(nèi)存的界限,具體來(lái)說(shuō)變量究竟分配到哪里,是由編譯器來(lái)決定的。

1逃逸分析escape analysis

所謂逃逸分析就是在編譯階段由編譯器根據(jù)變量的類型、外部使用情況等因素來(lái)判定是分配到堆還是棧,從而替代人工處理。

一般將局部變量和參數(shù)分配到棧上,但是并不絕對(duì):

  • 如果編譯器不能確定在函數(shù)返回時(shí),變量是否被使用則分配到堆上
  • 如果局部變量非常大,也會(huì)分配到堆上
  • ......

編譯器不清楚局部變量是否會(huì)被外部使用時(shí),就會(huì)傾向于分配到堆上。

Go編譯器在確定函數(shù)返回后不會(huì)再被引用時(shí)才分配到棧上,其他情況下都是分配到堆上。

這樣做雖然浪費(fèi)堆空間,但是有效避免了懸掛指針的出現(xiàn),并且由于GC的存在也不會(huì)出現(xiàn)內(nèi)存泄漏,權(quán)衡之下也是一種合理的做法。

2哪些情況會(huì)出現(xiàn)內(nèi)存逃逸

對(duì)于Go來(lái)說(shuō),在日常使用中有幾種常見的做法會(huì)導(dǎo)致內(nèi)存逃逸現(xiàn)象的出現(xiàn):

  • 指針逃逸
  • ??臻g不足逃逸
  • map/slice/interface/channel的使用
  • ......

指針逃逸

在上一個(gè)例子中我們使用一個(gè)int指針來(lái)說(shuō)明內(nèi)存逃逸的現(xiàn)象,接下來(lái)我們擴(kuò)展一下變?yōu)榻Y(jié)構(gòu)體指針,并且使用gcflags來(lái)給編譯器傳特定參數(shù)來(lái)觀察逃逸現(xiàn)象:

  1. // test.go 
  2. package main 
  3.  
  4. import "fmt" 
  5.  
  6. type Escape struct { 
  7.  who string 
  8.  
  9. func CallInstance(caller string) (*Escape) { 
  10.  instance := new(Escape
  11.  instance.who = caller 
  12.  return instance 
  13.  
  14. func main() { 
  15.  outer := CallInstance("hello world"
  16.  fmt.Println(outer.who) 

執(zhí)行:go build -gcflags=-m test.go 如下:

  1. # command-line-arguments 
  2. ./test.go:9:6: can inline CallInstance 
  3. ./test.go:16:23: inlining call to CallInstance 
  4. ./test.go:17:13: inlining call to fmt.Println 
  5. ./test.go:9:19: leaking param: caller 
  6. ./test.go:10:17: new(Escape) escapes to heap 
  7. ./test.go:16:23: main new(Escape) does not escape 
  8. ./test.go:17:19: outer.who escapes to heap 
  9. ./test.go:17:13: main []interface {} literal does not escape 
  10. ./test.go:17:13: io.Writer(os.Stdout) escapes to heap 
  11. <autogenerated>:1: (*File).close .this does not escape 

我們可以看到"escapes to heap",確實(shí)出現(xiàn)了內(nèi)存逃逸,本該在棧上逃逸到堆上了。

??臻g不足逃逸

對(duì)于64bit的Linux系統(tǒng)而言棧的大小一般是8MB,Go中每個(gè)goroutine初始化棧大小是2KB,在goroutine的運(yùn)行過程中棧的大小可能會(huì)變化,但也不會(huì)超過OS對(duì)線程棧大小的限制。

在網(wǎng)上找了個(gè)例子,用mac跑了一下:

  1. package main 
  2.  
  3. import "math/rand" 
  4.  
  5. func generate8191() { 
  6.  nums := make([]int, 8191) // < 64KB 
  7.  for i := 0; i < 8191; i++ { 
  8.   nums[i] = rand.Int() 
  9.  } 
  10.  
  11. func generate8192() { 
  12.  nums := make([]int, 8192) // = 64KB 
  13.  for i := 0; i < 8192; i++ { 
  14.   nums[i] = rand.Int() 
  15.  } 
  16.  
  17. func generate(n int) { 
  18.  nums := make([]int, n) // 不確定大小 
  19.  for i := 0; i < n; i++ { 
  20.   nums[i] = rand.Int() 
  21.  } 
  22.  
  23. func main() { 
  24.     generate8191() 
  25.     generate8192() 
  26.     generate(1) 
  1. # command-line-arguments 
  2. ./test_3.go:6:14: generate8191 make([]int, 8191) does not escape 
  3. ./test_3.go:13:14: make([]int, 8192) escapes to heap 
  4. ./test_3.go:20:14: make([]int, n) escapes to heap 

可以看到在分配8191個(gè)大小時(shí)未發(fā)生逃逸,在分配8192時(shí)發(fā)生了逃逸,不定長(zhǎng)度也發(fā)生了逃逸。

其他情況

在go中map、interface、slice、interface是非常常見的數(shù)據(jù)結(jié)構(gòu),也是非常容易觸發(fā)內(nèi)存逃逸的根源。

  • 向channel中發(fā)送指針或者帶指針的值,因?yàn)樵诰幾g時(shí)沒有辦法知道哪個(gè)goroutine會(huì)在channel上接收數(shù)據(jù)。所以編譯器沒法知道變量什么時(shí)候才會(huì)被釋放。
  • slice中指針或帶指針的值,這會(huì)導(dǎo)致切片的內(nèi)容逃逸,盡管其后面的數(shù)組可能是在棧上分配的,但其引用的值一定是在堆上。
  • slice數(shù)組擴(kuò)容也可能導(dǎo)致內(nèi)存逃逸,如果切片背后的存儲(chǔ)要基于運(yùn)行時(shí)的數(shù)據(jù)進(jìn)行擴(kuò)充,就會(huì)在堆上分配。
  • interface類型可以代表任意類型,編譯器不知道參數(shù)會(huì)是什么類型,只有運(yùn)行時(shí)才知道,因此只能分配到堆上。

Part3內(nèi)存逃逸小結(jié)

我們?cè)撊绾卧u(píng)價(jià)內(nèi)存逃逸呢?

  • Go語(yǔ)言對(duì)用戶來(lái)說(shuō)模糊了堆內(nèi)存和棧內(nèi)存的分配,編譯器借助于逃逸分析來(lái)實(shí)現(xiàn)特定場(chǎng)景的內(nèi)存逃逸。
  • 任何事情都是兩面性,Go語(yǔ)言借助于內(nèi)存逃逸和GC機(jī)制解放了程序員,但是同時(shí)也帶來(lái)了性能問題,因?yàn)槎褍?nèi)存的分配和釋放都是需要成本的。
  • Go的編譯器在很多時(shí)候無(wú)法確定該如何分配內(nèi)存,因此只能采用一種穩(wěn)妥但有失性能的做法,分配到堆上。
  • 意識(shí)里指針傳遞比值傳遞更高效,但是在Go中并非如此,如果指針傳遞出現(xiàn)內(nèi)存逃逸將內(nèi)存分配到堆上后續(xù)就有會(huì)GC操作,消耗比值傳遞更大。
  • 如果明確不需要外部使用,就需要盡量避免內(nèi)存逃逸,不要一味完全依賴編譯器本身。

 

責(zé)任編輯:武曉燕 來(lái)源: 后端技術(shù)指南針
相關(guān)推薦

2022-07-25 15:38:59

Go 語(yǔ)言Go 語(yǔ)言編譯器內(nèi)存逃逸

2024-04-07 11:33:02

Go逃逸分析

2023-01-10 09:18:37

Go內(nèi)存分配逃逸

2023-01-28 08:32:04

Go內(nèi)存分配

2022-07-10 23:15:46

Go語(yǔ)言內(nèi)存

2022-11-30 08:19:15

內(nèi)存分配Go逃逸分析

2017-03-17 09:31:40

2021-01-06 09:47:51

內(nèi)存Go語(yǔ)言

2023-11-21 15:46:13

Go內(nèi)存泄漏

2017-10-23 14:08:37

2021-12-28 17:39:05

Go精度Json

2018-03-12 22:13:46

GO語(yǔ)言編程軟件

2010-03-15 16:06:52

2022-11-08 11:26:13

Go逃逸代碼

2014-01-14 09:10:53

GoHTTP內(nèi)存泄漏

2011-07-06 12:04:53

架構(gòu)

2023-12-22 07:55:38

Go語(yǔ)言分配策略

2021-08-02 07:57:02

內(nèi)存Go語(yǔ)言

2012-03-28 09:48:45

2017-10-27 14:32:53

內(nèi)存存儲(chǔ)暴漲
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)