Golang之變數去哪兒

来源:https://www.cnblogs.com/qcrao-2018/archive/2019/02/28/10453260.html
-Advertisement-
Play Games

寫過C/C++的同學都知道,調用著名的malloc和new函數可以在堆上分配一塊記憶體,這塊記憶體的使用和銷毀的責任都在程式員。一不小心,就會發生記憶體泄露,搞得膽戰心驚。切換到Golang後,基本不會擔心記憶體泄露了。雖然也有new函數,但是使用new函數得到的記憶體不一定就在堆上。逃逸分析告訴你變數到底去... ...


目錄

寫過C/C++的同學都知道,調用著名的malloc和new函數可以在堆上分配一塊記憶體,這塊記憶體的使用和銷毀的責任都在程式員。一不小心,就會發生記憶體泄露,搞得膽戰心驚。

切換到Golang後,基本不會擔心記憶體泄露了。雖然也有new函數,但是使用new函數得到的記憶體不一定就在堆上。堆和棧的區別對程式員“模糊化”了,當然這一切都是Go編譯器在背後幫我們完成的。

一個變數是在堆上分配,還是在棧上分配,是經過編譯器的逃逸分析之後得出的結論。

這篇文章,就將帶領大家一起去探索逃逸分析——變數到底去哪兒,堆還是棧?

什麼是逃逸分析

以前寫C/C++代碼時,為了提高效率,常常將pass-by-value(傳值)“升級”成pass-by-reference,企圖避免構造函數的運行,並且直接返回一個指針。

你一定還記得,這裡隱藏了一個很大的坑:在函數內部定義了一個局部變數,然後返回這個局部變數的地址(指針)。這些局部變數是在棧上分配的(靜態記憶體分配),一旦函數執行完畢,變數占據的記憶體會被銷毀,任何對這個返回值作的動作(如解引用),都將擾亂程式的運行,甚至導致程式直接崩潰。比如下麵的這段代碼:

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

有些同學可能知道上面這個坑,用了個更聰明的做法:在函數內部使用new函數構造一個變數(動態記憶體分配),然後返回此變數的地址。因為變數是在堆上創建的,所以函數退出時不會被銷毀。但是,這樣就行了嗎?new出來的對象該在何時何地delete呢?調用者可能會忘記delete或者直接拿返回值傳給其他函數,之後就再也不能delete它了,也就是發生了記憶體泄露。關於這個坑,大家可以去看看《Effective C++》條款21,講得非常好!

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

你錶面的光鮮,一定是背後有很多人為你撐起的!Go語言里就是編譯器的逃逸分析。它是編譯器執行靜態代碼分析後,對記憶體管理進行的優化和簡化。

在編譯原理中,分析指針動態範圍的方法稱之為逃逸分析。通俗來講,當一個對象的指針被多個方法或線程引用時,我們稱這個指針發生了逃逸。

更簡單來說,逃逸分析決定一個變數是分配在堆上還是分配在棧上。

為什麼要逃逸分析

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

C/C++中動態分配的記憶體需要我們手動釋放,導致猿們平時在寫程式時,如履薄冰。這樣做有他的好處:程式員可以完全掌控記憶體。但是缺點也是很多的:經常出現忘記釋放記憶體,導致記憶體泄露。所以,很多現代語言都加上了垃圾回收機制。

Go的垃圾回收,讓堆和棧對程式員保持透明。真正解放了程式員的雙手,讓他們可以專註於業務,“高效”地完成代碼編寫。把那些記憶體管理的複雜機制交給編譯器,而程式員可以去享受生活。

逃逸分析這種“騷操作”把變數合理地分配到它該去的地方,“找準自己的位置”。即使你是用new申請到的記憶體,如果我發現你竟然在退出函數後沒有用了,那麼就把你丟到棧上,畢竟棧上的記憶體分配比堆上快很多;反之,即使你錶面上只是一個普通的變數,但是經過逃逸分析後發現在退出函數之後還有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前實現共產主義!

如果變數都分配到堆上,堆不像棧可以自動清理。它會引起Go頻繁地進行垃圾回收,而垃圾回收會占用比較大的系統開銷(占用CPU容量的25%)。

堆和棧相比,堆適合不可預知大小的記憶體分配。但是為此付出的代價是分配速度較慢,而且會形成記憶體碎片。棧記憶體分配則會非常快。棧分配記憶體只需要兩個CPU指令:“PUSH”和“RELEASSE”,分配和釋放;而堆分配記憶體首先需要去找到一塊大小合適的記憶體塊,之後要通過垃圾回收才能釋放。

通過逃逸分析,可以儘量把那些不需要分配到堆上的變數直接分配到棧上,堆上的變數少了,會減輕分配堆記憶體的開銷,同時也會減少gc的壓力,提高程式的運行速度。

逃逸分析是怎麼完成的

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

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

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

對一個變數取地址,可能會被分配到堆上。但是編譯器進行逃逸分析後,如果考察到在函數返回後,此變數不會被引用,那麼還是會被分配到棧上。套個取址符,就想騙補助?Too young!

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

  1. 如果函數外部沒有引用,則優先放到棧中;
  2. 如果函數外部存在引用,則必定放到堆中;

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

逃逸分析實例

Go提供了相關的命令,可以查看變數是否發生逃逸。

還是用上面我們提到的例子:

package main

import "fmt"

func foo() *int {
    t := 3
    return &t;
}

func main() {
    x := foo()
    fmt.Println(*x)
}

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

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

-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{}),編譯期間很難確定其參數的具體類型,也會發生逃逸。

使用反彙編命令也可以看出變數是否發生逃逸。

go tool compile -S main.go

截取部分結果,圖中標記出來的說明t是在堆上分配記憶體,發生了逃逸。
反彙編

總結

堆上動態分配記憶體比棧上靜態分配記憶體,開銷大很多。

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

Go編譯器會在編譯期對考察變數的作用域,並作一系列檢查,如果它的作用域在運行期間對編譯器一直是可知的,那麼就會分配到棧上。

簡單來說,編譯器會根據變數是否被外部引用來決定是否逃逸。對於Go程式員來說,編譯器的這些逃逸分析規則不需要掌握,我們只需通過go build -gcflags '-m'命令來觀察變數逃逸情況就行了。

不要盲目使用變數的指針作為函數參數,雖然它會減少複製操作。但其實當參數為變數自身的時候,複製是在棧上完成的操作,開銷遠比變數逃逸後動態地在堆上分配記憶體少的多。

最後,儘量寫出少一些逃逸的代碼,提升程式的運行效率。

QR

參考資料

【逃逸是怎麼發生的?很贊 結尾有很多參考資料】https://www.do1618.com/archives/1328/go-%E5%86%85%E5%AD%98%E9%80%83%E9%80%B8%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/

【Go的變數到底在堆還是棧中分配】https://github.com/developer-learning/night-reading-go/blob/master/content/discuss/2018-07-09-make-new-in-go.md

【Golang堆棧的理解】https://segmentfault.com/a/1190000017498101

【逃逸分析 編寫棧分配記憶體建議】https://segment.com/blog/allocation-efficiency-in-high-performance-go-services/
【逃逸分析 比較簡潔】https://studygolang.com/articles/17584

【逃逸分析定義】https://cloud.tencent.com/developer/article/1117410

【逃逸分析例子】https://my.oschina.net/renhc/blog/2222104

https://gocn.vip/article/355
【彙編代碼 傳參】https://github.com/maniafish/about_go/blob/master/heap_stack.md

【逃逸分析的缺陷】https://studygolang.com/articles/12396

【比較好的逃逸分析的例子】http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [TOC] python裝飾器初級 認識裝飾器 概念: 簡單地說: 原則 : 不修改被裝飾函數的源代碼 不修改被裝飾函數的調用方式 優點: 有助於讓我們的代碼更簡短,也更Pythonic(Python範兒 應用場景: 在項目迭代過程中,需要不停的為某一個功能(函數)新增或刪除某些小功能, 如果可復用 ...
  • 大一生活真 特麽 ”豐富多彩“ ,多彩到我要忙到哭泣,身為班長,很多班級的事情需要管理,也是,什麼東西都得體驗學一學,從學生會主席、團委團總支、社團社長都體驗過一番了,現在差個班長也沒試過,就來體驗了一番哈哈哈,其實這種精心服務一個班級的人還是很棒的一種感覺呢。思考思考最近的任務啊: (1)英語劇 ...
  • 一等對象 什麼是一等對象: 在運行時創建 能賦值給變數或數據結構中的元素 能作為參數傳遞給函數 能作為函數的返回結果 python中的字元串,列表什麼的都是一等對象,但對如果之前只是使用c++、java語言的人們來說python中的函數也是一等對象,那一定會有一點不可思議 接下來就介紹一下這個一等對 ...
  • import java.sql.Connection; import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;imp ...
  • 作者:JavaGuide(公眾號) 下麵這些問題都是一線大廠的真實面試問題,不論是對你面試還是說拓寬知識面都很有幫助。之前發過一篇8 張圖讀懂大型網站技術架構 可以作為不太瞭解大型網站系統技術架構朋友的入門文章。 文章目錄1. 你使用過哪些組件或者方法來提升網站性能,可用性以及併發量2. 設計高可用 ...
  • 1.使用cookie代替session(不安全,不推薦使用) 2.使用資料庫存儲session(效率低,不推薦使用) 3.使用nginx反向代理ip綁定方法,同一個ip只能在同一臺伺服器上進行訪問(不推薦,相當於沒有集群)。 4.使用Spring-Session框架,相當於把session緩存到re ...
  • 音頻系統工具箱™針對實時音頻處理進行了優化。audioDeviceReader, audioDeviceWriter, audioPlayerRecorder, dsp.AudioFileReader和dsp.AudioFileWriter器是為流式傳輸多通道音頻而設計的, 它們提供了必要的參數, ...
  • import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement;import java.sql.SQLException;import java.sql.Timestamp;im ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...