濮阳杆衣贸易有限公司

主頁 > 知識庫 > GoLang 逃逸分析的機(jī)制詳解

GoLang 逃逸分析的機(jī)制詳解

熱門標(biāo)簽:長沙ai機(jī)器人電銷 福州鐵通自動(dòng)外呼系統(tǒng) 智能電銷機(jī)器人營銷 廣東語音外呼系統(tǒng)供應(yīng)商 濮陽自動(dòng)外呼系統(tǒng)代理 地圖標(biāo)注測試 澳門防封電銷卡 烏魯木齊人工電銷機(jī)器人系統(tǒng) 賺地圖標(biāo)注的錢犯法嗎

對于手動(dòng)管理內(nèi)存的語言,比如 C/C++,調(diào)用著名的malloc和new函數(shù)可以在堆上分配一塊內(nèi)存,這塊內(nèi)存的使用和銷毀的責(zé)任都在程序員。一不小心,就會發(fā)生內(nèi)存泄露,搞得膽戰(zhàn)心驚。

但是 Golang 并不是這樣,雖然 Golang 語言里面也有 new。Golang 編譯器決定變量應(yīng)該分配到什么地方時(shí)會進(jìn)行逃逸分析。使用new函數(shù)得到的內(nèi)存不一定就在堆上。堆和棧的區(qū)別對程序員“模糊化”了,當(dāng)然這一切都是Go編譯器在背后幫我們完成的。一個(gè)變量是在堆上分配,還是在棧上分配,是經(jīng)過編譯器的逃逸分析之后得出的結(jié)論。

一、 逃逸分析是什么

wiki定義

In compiler optimization, escape analysis is a method for determining the dynamic scope of pointers - where in the program a pointer can be accessed. It is related to pointer analysis and shape analysis.

When a variable (or an object) is allocated in a subroutine, a pointer to the variable can escape to other threads of execution, or to calling subroutines. If an implementation uses tail call optimization (usually required for functional languages), objects may also be seen as escaping to called subroutines. If a language supports first-class continuations (as do Scheme and Standard ML of New Jersey), portions of the call stack may also escape.

If a subroutine allocates an object and returns a pointer to it, the object can be accessed from undetermined places in the program — the pointer has "escaped". Pointers can also escape if they are stored in global variables or other data structures that, in turn, escape the current procedure.

Escape analysis determines all the places where a pointer can be stored and whether the lifetime of the pointer can be proven to be restricted only to the current procedure and/or threa.

C/C++中,有時(shí)為了提高效率,常常將pass-by-value(傳值)“升級”成pass-by-reference,企圖避免構(gòu)造函數(shù)的運(yùn)行,并且直接返回一個(gè)指針。然而這里隱藏了一個(gè)很大的坑:在函數(shù)內(nèi)部定義了一個(gè)局部變量,然后返回這個(gè)局部變量的地址(指針)。這些局部變量是在棧上分配的(靜態(tài)內(nèi)存分配),一旦函數(shù)執(zhí)行完畢,變量占據(jù)的內(nèi)存會被銷毀,任何對這個(gè)返回值作的動(dòng)作(如解引用),都將擾亂程序的運(yùn)行,甚至導(dǎo)致程序直接崩潰。例如:

int *foo ( void )  
{  
  int t = 3;
  return t;
}

為了避免這個(gè)坑,有個(gè)更聰明的做法:在函數(shù)內(nèi)部使用new函數(shù)構(gòu)造一個(gè)變量(動(dòng)態(tài)內(nèi)存分配),然后返回此變量的地址。因?yàn)樽兞渴窃诙焉蟿?chuàng)建的,所以函數(shù)退出時(shí)不會被銷毀。但是,這樣就行了嗎?new出來的對象該在何時(shí)何地delete呢?調(diào)用者可能會忘記delete或者直接拿返回值傳給其他函數(shù),之后就再也不能delete它了,也就是發(fā)生了內(nèi)存泄露。關(guān)于這個(gè)坑,大家可以去看看《Effective C++》條款21,講得非常好!

C++是公認(rèn)的語法最復(fù)雜的語言,據(jù)說沒有人可以完全掌握C++的語法。而這一切在Go語言中就大不相同了。像上面示例的C++代碼放到Go里,沒有任何問題。

你表面的光鮮,一定是背后有很多人為你撐起的!Go語言里就是編譯器的逃逸分析。它是編譯器執(zhí)行靜態(tài)代碼分析后,對內(nèi)存管理進(jìn)行的優(yōu)化和簡化。

在編譯原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析。通俗來講,當(dāng)一個(gè)對象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸。

更簡單來說,逃逸分析決定一個(gè)變量是分配在堆上還是分配在棧上。

二、 為什么要逃逸分析

前面講的C/C++中出現(xiàn)的問題,在Go中作為一個(gè)語言特性被大力推崇。真是C/C++之砒霜Go之蜜糖!

C/C++中動(dòng)態(tài)分配的內(nèi)存需要我們手動(dòng)釋放,導(dǎo)致猿們平時(shí)在寫程序時(shí),如履薄冰。這樣做有他的好處:程序員可以完全掌控內(nèi)存。但是缺點(diǎn)也是很多的:經(jīng)常出現(xiàn)忘記釋放內(nèi)存,導(dǎo)致內(nèi)存泄露。所以,很多現(xiàn)代語言都加上了垃圾回收機(jī)制。

Go的垃圾回收,讓堆和棧對程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于業(yè)務(wù),“高效”地完成代碼編寫。把那些內(nèi)存管理的復(fù)雜機(jī)制交給編譯器,而程序員可以去享受生活。

逃逸分析這種“騷操作”把變量合理地分配到它該去的地方,“找準(zhǔn)自己的位置”。即使你是用new申請到的內(nèi)存,如果我發(fā)現(xiàn)你竟然在退出函數(shù)后沒有用了,那么就把你丟到棧上,畢竟棧上的內(nèi)存分配比堆上快很多;反之,即使你表面上只是一個(gè)普通的變量,但是經(jīng)過逃逸分析后發(fā)現(xiàn)在退出函數(shù)之后還有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前實(shí)現(xiàn)共產(chǎn)主義!

如果變量都分配到堆上,堆不像??梢宰詣?dòng)清理。它會引起Go頻繁地進(jìn)行垃圾回收,而垃圾回收會占用比較大的系統(tǒng)開銷(占用CPU容量的25%)。

堆和棧相比,堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價(jià)是分配速度較慢,而且會形成內(nèi)存碎片。棧內(nèi)存分配則會非???。棧分配內(nèi)存只需要兩個(gè)CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊,之后要通過垃圾回收才能釋放。

通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會減輕分配堆內(nèi)存的開銷,同時(shí)也會減少gc的壓力,提高程序的運(yùn)行速度。

三、 逃逸分析如何完成

Go逃逸分析最基本的原則是:如果一個(gè)函數(shù)返回對一個(gè)變量的引用,那么它就會發(fā)生逃逸。

簡單來說,編譯器會分析代碼的特征和代碼生命周期,Go中的變量只有在編譯器可以證明在函數(shù)返回后不會再被引用的,才分配到棧上,其他情況下都是分配到堆上。

Go語言里沒有一個(gè)關(guān)鍵字或者函數(shù)可以直接讓變量被編譯器分配到堆上,相反,編譯器通過分析代碼來決定將變量分配到何處。

對一個(gè)變量取地址,可能會被分配到堆上。但是編譯器進(jìn)行逃逸分析后,如果考察到在函數(shù)返回后,此變量不會被引用,那么還是會被分配到棧上。

簡單來說,編譯器會根據(jù)變量是否被外部引用來決定是否逃逸:

1)如果函數(shù)外部沒有引用,則優(yōu)先放到棧中;

2) 如果函數(shù)外部存在引用,則必定放到堆中;

針對第一條,可能放到堆上的情形:定義了一個(gè)很大的數(shù)組,需要申請的內(nèi)存過大,超過了棧的存儲能力。

四、 逃逸分析實(shí)例

下面是一個(gè)簡單的例子。

package main

import ()

func foo() *int {
  var x int
  return x
}

func bar() int {
  x := new(int)
  *x = 1
  return *x
}

func main() {}

開啟逃逸分析日志很簡單,只要在編譯的時(shí)候加上-gcflags '-m',但是我們?yōu)榱瞬蛔尵幾g時(shí)自動(dòng)內(nèi)連函數(shù),一般會加-l參數(shù),最終為-gcflags '-m -l',執(zhí)行如下命令:

$ go build -gcflags '-m -l' main.go
# command-line-arguments
./main.go:5:9: x escapes to heap
./main.go:4:6: moved to heap: x
./main.go:9:10: bar new(int) does not escape

上面代碼中foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了棧上。

也可以使用反匯編命令看出變量是否發(fā)生逃逸。

$ go tool compile -S main.go

截取部分結(jié)果,圖中標(biāo)記出來的說明foo中x是在堆上分配內(nèi)存,發(fā)生了逃逸。

反匯編命令結(jié)果

什么時(shí)候逃逸呢? golang.org FAQ 上有一個(gè)關(guān)于變量分配的問題如下:

Q: How do I know whether a variable is allocated on the heap or the stack?

A: From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

關(guān)于什么時(shí)候逃逸,什么時(shí)候不逃逸,我們接下來再看幾個(gè)小例子。

1)Example1

package main
type S struct{}
func main() {
  var x S
  y := x
  _ = *identity(y)
}
func identity(z *S) *S {
  return z
}

結(jié)果如下:

# command-line-arguments
./main.go:8:22: leaking param: z to result ~r1 level=0
./main.go:5:7: main x does not escape

這里的第一行表示z變量是“流式”,因?yàn)閕dentity這個(gè)函數(shù)僅僅輸入一個(gè)變量,又將這個(gè)變量作為返回輸出,但identity并沒有引用z,所以這個(gè)變量沒有逃逸,而x沒有被引用,且生命周期也在mian里,x沒有逃逸,分配在棧上。

2)Example2

package main
type S struct{}
func main() {
  var x S
  _ = *ref(x)
}
func ref(z S) *S {
  return z
}

結(jié)果如下:

# command-line-arguments
./main.go:8:9: z escapes to heap
./main.go:7:16: moved to heap: z

這里的z是逃逸了,原因很簡單,go都是值傳遞,ref函數(shù)copy了x的值,傳給z,返回z的指針,然后在函數(shù)外被引用,說明z這個(gè)變量在函數(shù)內(nèi)聲明,可能會被函數(shù)外的其他程序訪問。所以z逃逸了,分配在堆上

3)Example3

package main
type S struct { 
  M *int
}
func main() { 
  var i int 
  refStruct(i)
}
func refStruct(y int) (z S) {
  z.M = y
  return z 
}

結(jié)果如下:

# command-line-arguments
./main.go:10:8: y escapes to heap
./main.go:9:26: moved to heap: y

看日志的輸出,這里的y是逃逸了,看來在struct里好像并沒有區(qū)別,有可能被函數(shù)外的程序訪問就會逃逸

4)Example4

package main
type S struct { 
  M *int
}
func main() { 
  var i int 
  refStruct(i)
}
func refStruct(y *int) (z S) {
  z.M = y
  return z 
}

結(jié)果如下:

# command-line-arguments
./main.go:9:27: leaking param: y to result z level=0
./main.go:7:12: main i does not escape

這里的y沒有逃逸,分配在棧上,原因和Example1是一樣的。

5)Example5

package main
type S struct { 
  M *int
}
func main() { 
  var x S
  var i int
  ref(i, x) 
}
func ref(y *int, z *S) { 
  z.M = y
}

結(jié)果如下:

# command-line-arguments
./main.go:10:21: leaking param: y
./main.go:10:21: ref z does not escape
./main.go:8:6: i escapes to heap
./main.go:7:6: moved to heap: i
./main.go:8:10: main x does not escape

這里的z沒有逃逸,而i卻逃逸了,這是因?yàn)間o的逃逸分析不知道z和i的關(guān)系,逃逸分析不知道參數(shù)y是z的一個(gè)成員,所以只能把它分配給堆。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

您可能感興趣的文章:
  • Golang極簡入門教程(一):基本概念
  • 理解Golang中的數(shù)組(array)、切片(slice)和map
  • golang gorm 操作mysql及gorm基本用法
  • golang語言中for循環(huán)語句用法實(shí)例
  • golang中interface接口的深度解析
  • Golang 中整數(shù)轉(zhuǎn)字符串的方法
  • Golang讀寫Excel的方法教程
  • Mac OS系統(tǒng)安裝golang教程
  • golang判斷chan channel是否關(guān)閉的方法
  • Golang的os標(biāo)準(zhǔn)庫中常用函數(shù)的整理介紹

標(biāo)簽:德州 廣西 西雙版納 阿克蘇 調(diào)研邀請 貴陽 太原 慶陽

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《GoLang 逃逸分析的機(jī)制詳解》,本文關(guān)鍵詞  GoLang,逃逸,分析,的,機(jī)制,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《GoLang 逃逸分析的機(jī)制詳解》相關(guān)的同類信息!
  • 本頁收集關(guān)于GoLang 逃逸分析的機(jī)制詳解的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章
    浑源县| 贵州省| 望谟县| 林口县| 济源市| 安宁市| 乐昌市| 庆元县| 邹城市| 临桂县| 铅山县| 东辽县| 宣汉县| 平原县| 彰化县| 麻城市| 哈尔滨市| 阿拉善左旗| 乌兰浩特市| 会东县| 滨州市| 岐山县| 长丰县| 乐东| 静乐县| 湘潭县| 云安县| 基隆市| 饶阳县| 抚州市| 孙吴县| 宣化县| 青铜峡市| 峨山| 威远县| 马山县| 南康市| 吴旗县| 穆棱市| 福州市| 泰兴市|