Go語言 逃 逸 分 析

逃逸分析是什么????????

??逃逸分析是編譯器用于決定變量分配到堆上還是棧上的一種行為。

????????一個變量是在堆上分配,還是在棧上分配,是經過編譯器的逃逸分析之后得出的“結論”。
Go 語言里編譯器的逃逸分析:它是編譯器執行靜態代碼分析后,對內存管理進行的優化和簡化。
在編譯原理中,分析指針動態范圍的方法被稱之為逃逸分析。通俗來講,當一個對象的指針被多個方法或線程引用時,則稱這個指針發生了逃逸。逃逸分析決定一個變量是分配在堆上還是分配在棧上。

逃逸分析有什么作用

????????函數的運行都是在棧上面運行的,在棧上面聲明臨時變量,分配內存,函數運行完畢之后,回收內存,每個函數的棧空間都是獨立的,其他函數是無法進行訪問,但是在某些情況下棧上面的數據需要在函數結束之后還能被訪問,這時候就會設計到內存逃逸了,什么是逃逸,就是抓不住

????????如果變量從棧上面逃逸,會跑到堆上面,棧上面的變量在函數結束的時候回自動回收,回收代價比較小,棧的內存分配和使用一般只需要兩個CPU指令"PUSH"和"RELEASE",分配和釋放,而堆分配內存,則是首先需要找到一塊大小合適的內存,之后通過GC回收才能釋放,對于這種情況,頻繁的使用垃圾回收,則會占用比較大的系統開銷,所以盡量分配內存到棧上面,減少gc的壓力,提高程序運行速度

????????Go 的垃圾回收,讓堆和棧對程序員保持透明。真正解放了程序員的雙手,讓他們可以專注于
業務,“高效”地完成代碼編寫,而把那些內存管理的復雜機制交給編譯器。
逃逸分析把變量合理地分配到它該去的地方,“找準自己的位置”。即使是用 new 函數申請到的內存,如果編譯器發現這塊內存在退出函數后就沒有使用了,那就分配到棧上,畢竟棧上的內存
分配比堆上快很多;反之,即使表面上只是一個普通的變量,但是經過編譯器的逃逸分析后發現,
在函數之外還有其他的地方在引用,那就分配到堆上
。真正地做到“按需分配”。
如果變量都分配到堆上,堆不像棧可以自動清理。就會引起 Go 頻繁地進行垃圾回收,而垃圾
回收會占用比較大的系統開銷。
堆和棧相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片;棧內存分配則會非常快。棧分配內存只需要通過 PUSH 指令,并且會被自動釋放;而堆分配內存首先需要去找到一個大小合適的內存塊,之后要通過垃圾回收才能釋放。
通過逃逸分析,可以盡量把那些不需要分配到堆上的變量直接分配到棧上,堆上的變量少了,會減輕堆內存分配的開銷,同時也會減少垃圾回收(Garbage Collection,GC)的壓力,提高程序的運行速度。

逃逸分析過程/是怎么完成的

????????Go 語言逃逸分析最基本的原則是:如果一個函數返回對一個變量的引用,那么這個變量就會發生逃逸。


在任何情況下,如果一個值被分配到了棧之外的地方,那么一定是到了堆上面。簡而概之:編譯器會分析代碼的特征和代碼的生命周期,Go 中的變量只有在編譯器可以證明在函數返回后不會再被引用的,才分配到棧上,其他情況下都是分配到堆上。
Go 語言里沒有一個關鍵字或者函數可以直接讓變量被編譯器分配到堆上。相反,編譯器通過
分析代碼來決定將變量分配到何處

對一個變量取地址,可能會被分配到堆上。但是編譯器進行逃逸分析后,如果考慮到在函數返回后,此變量不會被引用,那么還是會被分配到棧上。簡單來說,編譯器會根據變量是否被外部引用來決定是否逃逸:

1)如果變量在函數外部沒有引用,則優先放到棧上。
2)如果變量在函數外部存在引用,則必定放到堆上。

針對第一條,放到堆上的情形:定義了一個很大的數組,需要申請的內存過大,超過了棧的存儲能力。

如何確定是否發生逃逸?

Go 提供了相關的命令,可以查看變量是否發生逃逸。使用前面提到的例子:

package main
import "fmt"
func foo() *int {t := 3return &t;
}
func main() {x := foo()fmt.Println(*x)
}

????????foo 函數返回一個局部變量的指針,使用 main 函數里變量 x 接收它。執行如下命令:

go build -gcflags '-m -l' main.go

????????其中 -gcflags 參數用于啟用編譯器支持的額外標志。例如,-m 用于輸出編譯器的優化細節
(包括使用逃逸分析這種優化),相反可以使用 -N 來關閉編譯器優化;而 -l 則用于禁用 foo 函數
的內聯優化,防止逃逸被編譯器通過內聯徹底的抹除。得到如下輸出:?

### command-line-arguments
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape

????????foo 函數里的變量 t 逃逸了,和預想的一致,不解的是為什么 main 函數里的 x 也逃逸了?
這是因為有些函數的參數為 interface 類型,比如 fmt.Println(a ...interface{}),編譯期間很難確定其
參數的具體類型,也會發生逃逸。?

指針逃逸

????????我們知道傳遞指針可以減少底層值的拷貝,可以提高效率,但是如果拷貝的數據量小,由于指針傳遞會產生逃逸,可能會使用堆,也可能會增加GC的負擔,所以傳遞指針不一定是高效的。

????????如下實例:

package maintype Student struct {Name stringAge  int
}func StudentRegister(name string, age int) *Student {s := new(Student) //局部變量s逃逸到堆s.Name = names.Age = agereturn s
}func main() {StudentRegister("Jim", 18)
}

????????雖然在函數 StudentRegister() 內部 s 為局部變量,其值通過函數返回值返回,s 本身為一指針,其指向的內存地址不會是棧而是堆,這就是典型的逃逸案例。?

棧空間不足

package mainfunc MakeSlice() {s := make([]int, 100, 100)for index, _ := range s {s[index] = index}
}func main() {MakeSlice()
}

此時棧空間充足,slice分配在棧上,未發生逃逸,假設將slice擴大100倍,再看一下

package mainfunc MakeSlice() {s := make([]int, 10000, 10000)for index, _ := range s {s[index] = index}
}func main() {MakeSlice()
}

此時,分配的slice容量太大,當棧空間不足以存放當前對象時或無法判斷當前切片長度時會將對象分配到堆中

動態類型逃逸?

很多函數參數為interface類型。比如:

func Printf(format string, a ...interface{}) (n int, err error)
func Sprintf(format string, a ...interface{}) string
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Print(a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

編譯期間很難確定其參數的具體類型,也能產生逃逸。

變量大小不確定?

????????在創建切片的時候,初始化切片容量的時候,傳入一個變量來指定其大小,由于變量的值不能在編譯器確定,所以就不能確定其占用空間的大小,直接將對象分配在堆上

package mainfunc MakeSlice() {length := 1a := make([]int, length, length)for i := 0; i < length; i++ {a[i] = i}
}func main() {MakeSlice()
}

逃逸常見情況

  1. 指針逃逸,函數內部返回一個局部變量指針

  2. 分配大對象,導致棧空間不足,不得不分配到堆上

  3. 調用接口類型的方法。接口類型的方法調用是動態調度 - 實際使用的具體實現只能在運行時確定。考慮一個接口類型為 io.Reader 的變量 r。對 r.Read(b) 的調用將導致 r 的值和字節片b的后續轉義并因此分配到堆上。

  4. 盡管能夠符合分配到棧的場景,但是其大小不能夠在在編譯時候確定的情況,也會分配到堆上

如何避免

  1. go 中的接口類型的方法調用是動態調度,因此不能夠在編譯階段確定,所有類型結構轉換成接口的過程會涉及到內存逃逸的情況發生。如果對于性能要求比較高且訪問頻次比較高的函數調用,應該盡量避免使用接口類型

  2. 由于切片一般都是使用在函數傳遞的場景下,而且切片在 append 的時候可能會涉及到重新分配內存,如果切片在編譯期間的大小不能夠確認或者大小超出棧的限制,多數情況下都會分配到堆上

總結

  1. 堆上動態分配內存比棧上靜態分配內存,開銷大很多。

  2. 變量分配在棧上需要能在編譯期確定它的作用域,否則會分配到堆上。

  3. Go編譯器會在編譯期對考察變量的作用域,并作一系列檢查,如果它的作用域在運行期間對編譯器一直是可知的,那么就會分配到棧上。簡單來說,編譯器會根據變量是否被外部引用來決定是否逃逸。

  4. 對于Go程序員來說,編譯器的這些逃逸分析規則不需要掌握,我們只需通過go build -gcflags '-m'命令來觀察變量逃逸情況就行了

  5. 不要盲目使用變量的指針作為函數參數,雖然它會減少復制操作。但其實當參數為變量自身的時候,復制是在棧上完成的操作,開銷遠比變量逃逸后動態地在堆上分配內存少的多。

下面代碼中的變量發生逃逸了嗎?

示例1:?

package main
type S struct {}func main() {var x S_ = identity(x)
}func identity(x S) S {return x
}

分析:Go語言函數傳遞都是通過值的,調用函數的時候,直接在棧上copy出一份參數,不存在逃逸。?

?示例2:

package maintype S struct {}func main() {var x Sy := &x_ = *identity(y)
}func identity(z *S) *S {return z
}

分析:identity函數的輸入直接當成返回值了,因為沒有對z作引用,所以z沒有逃逸。對x的引用也沒有逃出main函數的作用域,因此x也沒有發生逃逸。

?示例3:

package maintype S struct {}func main() {var x S_ = *ref(x)
}func ref(z S) *S {return &z
}

分析:z是對x的拷貝,ref函數中對z取了引用,所以z不能放在棧上,z必須要逃逸到堆上。否則在ref函數之外,通過引用如何找到z。僅管在main函數中,直接丟棄了ref的結果,但是Go的編譯器還沒有那么智能,分析不出來這種情況。而對x從來就沒有取引用,所以x不會發生逃逸。?

?示例4:如果對一個結構體成員賦引用會如何

package maintype S struct {M *int
}func main() {var i intrefStruct(i)
}func refStruct(y int) (z S) {z.M = &yreturn z
}

分析:refStruct函數對y取了引用,所以y發生了逃逸。?

示例5:

package maintype S struct {M *int
}func main() {var i intrefStruct(&i)
}func refStruct(y *int) (z S) {z.M = yreturn z
}

分析:在main函數里對i取了引用,并且把它傳給了refStruct函數,i的引用一直在main函數的作用域用,因此i沒有發生逃逸。和上一個例子相比,有一點小差別,但是導致的程序效果是不同的:例子4中,i先在main的棧幀中分配,之后又在refStruct棧幀中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通過引用傳遞。?

?示例6:

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

分析:本例i發生了逃逸,按照前面例子5的分析,i不會逃逸。兩個例子的區別是例子5中的S是在返回值里的,輸入只能“流入”到輸出,本例中的S是在輸入參數中,所以逃逸分析失敗,i要逃逸到堆上。

?Go 與C/C++中的堆和棧是同一個概念嗎

????????在前面的分析中,其實隱式地默認了所提及Go 中堆和棧這些概念與 C/C++ 中堆和棧的概念
是同一種事物。但讀者應該需要進一步認識到這里面的區別。
首先要明確,C/C++ 中提及的“程序堆棧”本質上其實是操作系統層級的概念,它通過
C/C++ 語言的編譯器和所在的系統環境來共同決定。在程序啟動時,操作系統會自動維護一個所啟動程序消耗內存的地址空間,并自動將這個空間從邏輯上劃分為堆內存空間和棧內存空間。這時,“棧”的概念是指程序運行時自動獲得的一小塊內存,而后續的函數調用所消耗的棧大小,會在編譯期間由編譯器決定,用于保存局部變量或者保存函數調用棧。如果在 C/C++ 中聲明一個局部變量,則會執行邏輯上的壓棧操作,在棧中記錄局部變量。而當局部變量離開作用域之后,所謂的自動釋放本質上是該位置的內存在下一次函數調用壓棧的過程中,可以被無條件的覆蓋;對于堆而
言,每當程序通過系統調用向操作系統申請內存時,會將所需的空間從維護的堆內存地址空間中分
配出去,而在歸還時則會將歸還的內存合并到所維護的地址空間中。
Go 程序也是運行在操作系統上的程序,自然同樣擁有前面提及的堆和棧的概念。但區別在于
傳統意義上的“棧”被 Go 語言的運行時全部消耗了,用于維護運行時各個組件之間的協調,例如調度器、垃圾回收、系統調用等。而對于用戶態的 Go 代碼而言,它們所消耗的“堆和棧”,其實只是 Go 運行時通過管理向操作系統申請的堆內存,構造的邏輯上的“堆和棧”,它們的本質都是從操作系統申請而來的堆內存由于用戶態 Go 程序的“棧空間”是由運行時管理堆內存得來,相較于只有 1MB 的 C/C++ 中的“棧”而言,Go 程序擁有“幾乎”無限的棧內存(1GB)。更進一步,對于用戶態 Go 代碼消耗的棧,Go 語言運行時會為了防止內存碎片化,會在適當的時候對整個棧進行深拷貝,將其整個復制到另一塊內存區域(當然,這個過程對用戶態的代碼是不可見的),這也是相較于傳統意義上棧是一塊固定分配好的內存所出現的另一處差異。也正是由于這個特點的存在,指針的算術運算不再能奏效,因為在沒有特殊說明的情況下,無法確定運算前后指針所指向的地址的內容是否已經被 Go 運行時移動。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/91865.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/91865.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/91865.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

LeetCode算法日記 - Day 1: 移動零、復寫零

目錄 1. 移動零 1.1 思路解析 1.2 代碼實現 2. 復寫零 2.1 思路解析 2.2 代碼實現 1. 移動零 283. 移動零 - 力扣&#xff08;LeetCode&#xff09; 給定一個數組 nums&#xff0c;編寫一個函數將所有 0 移動到數組的末尾&#xff0c;同時保持非零元素的相對順序。 請…

Odoo:免費開源的醫療器械行業解決方案

開源智造Odoo專家團隊深知&#xff0c;作為醫療器械制造商&#xff0c;您的成功取決于制造卓越產品的能力。您必須遵循嚴密控制的流程&#xff0c;開發和制造出達到最嚴格質量標準的產品。“開源智造Odoo醫療器械行業解決方案”是為醫療器械制造商設計的全球企業資源規劃(ERP)軟…

Redis鍵值對中值的數據結構

前言 前面我們已經介紹了Redis的鍵值對存儲管理的底層數據結構。如果不清楚的同志可以看我前面的博客 Redis數據庫存儲鍵值對的底層原理-CSDN博客 下面,我們來看一下Redis鍵值對中值的數據結構有那些叭 Redis常見的5種數據類型 string …

MySQL自動化安裝工具-mysqldeploy

功能 可在linux系統上安裝 mysql5.5/5.6/5.7/8.0/8.4 版本的 MySQL&#xff0c;可以初始化多實例 MySQL。 碼云: https://gitee.com/hh688/mysqldeploy guithub: https://github.com/hhkens/mysqldeploy 限制 僅在 centos7 環境進行測試&#xff0c;后期可能支持更多系統。 此程…

簡要探討大型語言模型(LLMs)的發展歷史

關注大型語言模型(LLMs) 簡要探討語言模型的發展歷史 理解Transformer架構的基本元素和注意力機制 了解不同類型的微調方法 語言模型的大小之分 在語言模型領域,“小”和“大”是相對概念。幾年前還被視為“巨大”的模型,如今已被認為相當小。該領域發展迅猛,從參數規模為…

Java試題-選擇題(2)

Java試題-選擇題(2) 題目 下列語句創建對象的總個數是: String s=“a”+“b”+"c”+“d”+"e” A.4 B.2 C.3 D.1 關于下面的程序段的說法正確的是()? File file1=new File(“e:\xxx\yyy\zzz");file1.mkdir(); A.如目錄e:\xxx\yyy\不存在,程序會拋出FileN…

揭秘動態測試:軟件質量的實戰防線

動態測試概述&#xff08;擴展版&#xff09; 目錄 動態測試概述&#xff08;擴展版&#xff09; 一、動態測試的定義與重要性 ? 二、動態測試類型 &#x1f50d; &#xff08;一&#xff09;功能測試 &#x1f9e9; &#xff08;二&#xff09;非功能測試 &#x1f4ca…

機器學習①【機器學習的定義以及核心思想、數據集:機器學習的“燃料”(組成和獲取)】

文章目錄先言一、什么是機器學習1.機器學習的定義以及核心思想2.機器學習的四大類型2.1監督學習&#xff08;Supervised Learning&#xff09;2.2半監督學習&#xff08;Midsupervised Learning&#xff09;2.3無監督學習&#xff08;Unsupervised Learning&#xff09;2.4強化…

GaussDB 數據庫架構師(十二) 資源規劃

1 硬件和軟件要求 1&#xff09;硬件配置示例 硬件配置示例設備類型 設備型號 數量 備注 計算節點 CPU&#xff1a; 2*64 Cores&#xff0c;Kunpeng 920 內存&#xff1a;32*32GB 系統盤&#xff1a;2*960GB SATA SSD 數據盤&#xff1a;24*960GB SATA SSD RAID卡&#x…

Linux系統文件與目錄內容檢索(Day.2)

一、文件和目錄內容檢索處理命令1、uniq去重語法uniq [options] [input_file [output_file]]選項選項作用-c進行計數&#xff0c;并刪除文件中重復出現的行-d僅顯示連續的重復行-u僅顯示出現一次的行-i忽略大小寫案例1、刪除輸入文件中的重復行sort input.txt | uniq2、僅顯示重…

如何選擇一個容易被搜索引擎發現的域名?

在這個數字化時代&#xff0c;域名不僅是企業線上身份的標識&#xff0c;更是影響網站搜索曝光率的關鍵因素。一個精心挑選的域名能為品牌帶來更多自然流量&#xff0c;下面我們就來探討幾個實用技巧。一、簡潔易記是王道好域名首先要讓人過目不忘。想象一下&#xff0c;當用戶…

樹形DP進階:結合dfn序的線性化樹問題求解技巧

樹形DP進階&#xff1a;結合dfn序的線性化樹問題求解技巧一、dfn序與樹的線性化1.1 dfn序的基本概念1.2 樹形DP結合dfn序的優勢二、核心應用&#xff1a;子樹區間的DP優化2.1 子樹權值和的快速查詢與更新問題描述結合dfn序的解法代碼實現&#xff08;前綴和版本&#xff09;優化…

九、Maven入門學習記錄

Maven介紹Maven作用統一項目結構Maven安裝&#xff08;注意配置阿里云私服時url要跟換成最新的&#xff09;IDEA創建Meavn項目Maven坐標介紹IDEA導入Maven項目依賴配置依賴傳遞依賴傳遞-排除依賴依賴范圍生命周期生命周期-執行特定生命周期生命周期-總結

中標喜訊 | 安暢檢測再下一城!斬獲重慶供水調度測試項目

安暢檢測在第三方檢測領域持續深耕&#xff0c;再傳捷報&#xff01;公司于2025年7月30日正式收到中標通知&#xff0c;成功拿下重慶水資源產業股份有限公司 “重慶西部科學城多水廠分區分壓供水優化調度研究項目&#xff08;軟件測試標段&#xff09;”。 此次中標不僅是市場…

銀河麒麟V10一鍵安裝DM8的腳本及高階運維SQL分享

介質下載地址名稱網址銀河麒麟高級服務器操作系統V10&#xff08;SP3&#xff09;用戶手冊https://www.kylinos.cn/support/document/60.htmlDM8 安裝手冊https://eco.dameng.com/document/dm/zh-cn/pm/install-uninstall.htmlDM 數據庫安裝&#xff08;Linux安裝&#xff09;h…

cobalt strike(CS)與Metasploit(MSF)聯動

CS —> MSF首先cs上創建一個http的外部監聽器。此時在CS服務端查看監聽的ip&#xff0c;發現并沒有開啟&#xff0c;需要到成功移交會話后才會啟動。netstat -tunlp | grep 7000在MSF中使用handler模塊&#xff0c;配置監聽。注意&#xff1a;目標機器的地址是rhost&#xf…

C# 類型

原文&#xff1a;C# 類型_w3cschool C#類型 類型定義值的藍圖。有不同的操作與不同類型相關聯。 在下面的示例中&#xff0c;我們使用兩個類型為int的常量&#xff0c;值為2 和 3。 static void Main() {int x 2 * 3;Console.WriteLine (x); } int 是一個表示整數值的構建…

確保TDesign Vue Next中t-color-picker組件在彈出顏色拾取面板時保證該面板不抖動方法參考

使用TDesign Vue Next中的組件t-color-picker時&#xff0c;在顏色面板彈出后&#xff0c;如果修改里面的顏色&#xff0c;發現這個顏色拾取面板會隨著顏色的改變位置不斷抖動&#xff0c;該問題由顯示顏色的數值文本的長度變化引起&#xff0c;因此要覆蓋組件內部顏色值文本的…

bypass

代碼解析修改自身bypass&#xff1a;第一句話$s"Declaring file object\n";定義一個s&#xff0c;值為Declaring file object第二句話$d$_SERVER[DOCUMENT_ROOT].$_SERVER[DOCUMENT_URI]; 不知道$_SERVER是什么&#xff0c;那就打印出來看看。輸入echo <pre>;…

C語言:構造類型學習

內容提要 構造類型 枚舉類型typedef 綜合案例&#xff1a;斗地主 構造類型 枚舉類型 建議&#xff1a;如果定義不相干的常理&#xff0c;使用宏定義&#xff08;符號常量&#xff09;&#xff1b;如果需要定義一組相關聯的常量&#xff0c;如月份0~11&#xff0c;星期0~6&#…