Go 語言機制之逃逸分析(Language Mechanics On Escape Analysis)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。## 前序(Prelude)本系列文章總共四篇,主要協助大家理解 Go 語言中一些文法結構和其背後的設計原則,包括指標、棧、堆、逃逸分析和值/指標傳遞。這是第二篇,主要介紹堆和逃逸分析。以下是本系列文章的索引:1. [Go 語言機制之棧與指標](https://studygolang.com/articles/12443)2. [Go 語言機制之逃逸分析](https://studygolang.com/articles/12444)3. [Go 語言機制之記憶體剖析](https://studygolang.com/articles/12445)4. [Go 語言機制之資料和文法的設計哲學](https://studygolang.com/articles/12487)## 介紹(Introduction)在四部分系列的第一部分,我用一個將值共用給 goroutine 棧的例子介紹了指標結構的基礎。而我沒有說的是值存在棧之上的情況。為了理解這個,你需要學習值儲存的另外一個位置:堆。有這個基礎,就可以開始學習逃逸分析。逃逸分析是編譯器用來決定你的程式中值的位置的過程。特別地,編譯器執行靜態程式碼分析,以確定一個構造體的執行個體化值是否會逃逸到堆。在 Go 語言中,你沒有可用的關鍵字或者函數,能夠直接讓編譯器做這個決定。只能夠通過你寫代碼的方式來作出這個決定。## 堆(Heaps)堆是記憶體的第二地區,除了棧之外,用來儲存值的地方。堆無法像棧一樣能自清理,所以使用這部分記憶體會造成很大的開銷(相比於使用棧)。重要的是,開銷跟 GC(垃圾收集),即被牽扯進來保證這部分地區乾淨的程式,有很大的關係。當垃圾收集程式運行時,它會佔用你的可用 CPU 容量的 25%。更有甚者,它會造成微秒級的 “stop the world” 的延時。擁有 GC 的好處是你可以不再關注堆記憶體的管理,這部分很複雜,是曆史上容易出錯的地方。在 Go 中,會將一部分值分配到堆上。這些分配給 GC 帶來了壓力,因為堆上沒有被指標索引的值都需要被刪除。越多需要被檢查和刪除的值,會給每次運行 GC 時帶來越多的工作。所以,分配演算法不斷地工作,以平衡堆的大小和它啟動並執行速度。## 共用棧(Sharing Stacks)在 Go 語言中,不允許 goroutine 中的指標指向另外一個 goroutine 的棧。這是因為當棧增長或者收縮時,goroutine 中的棧記憶體會被一塊新的記憶體替換。如果運行時需要追蹤指標指向其他的 goroutine 的棧,就會造成非常多需要管理的記憶體,以至於更新指向那些棧的指標將使 “stop the world” 問題更嚴重。這裡有一個棧被替換好幾次的例子。看輸出的第 2 和第 6 行。你會看到 main 函數中的棧的字串地址值改變了兩次。[https://play.golang.org/p/pxn5u4EBSI](https://play.golang.org/p/pxn5u4EBSI)## 逃逸機制(Escape Mechanics)任何時候,一個值被分享到函數棧幀範圍之外,它都會在堆上被重新分配。這是逃逸分析演算法發現這些情況和管控這一層的工作。(記憶體的)完整性在於確保對任何值的訪問始終是準確、一致和高效的。通過查看這個語言機制瞭解逃逸分析。[https://play.golang.org/p/Y_VZxYteKO](https://play.golang.org/p/Y_VZxYteKO)### 清單 1```gopackage maintype user struct { name string email string}func main() { u1 := createUserV1() u2 := createUserV2() println("u1", &u1, "u2", &u2)}//go:noinlinefunc createUserV1() user { u := user{ name: "Bill", email: "bill@ardanlabs.com", } println("V1", &u) return u}//go:noinlinefunc createUserV2() *user { u := user{ name: "Bill", email: "bill@ardanlabs.com", } println("V2", &u) return &u}```我使用 `go:noinline` 指令,阻止在 `main` 函數中,編譯器使用內聯代碼替代函數調用。內聯(最佳化)會使函數調用消失,並使例子複雜化。我將在下一篇博文介紹內聯造成的副作用。在表 1 中,你可以看到建立 `user` 值,並返回給調用者的兩個不同的函數。在函數版本 1 中,傳回值。### 清單 2```16 func createUserV1() user {17 u := user{18 name: "Bill",19 email: "bill@ardanlabs.com",20 }2122 println("V1", &u)23 return u24 }```我說這個函數返回的是值是因為這個被函數建立的 `user` 值被拷貝並傳遞到調用棧上。這意味著調用函數接收到的是這個值的拷貝。你可以看下第 17 行到 20 行 `user` 值被構造的過程。然後在第 23 行,`user` 值的副本被傳遞到調用棧並返回給調用者。函數返回後,棧看起來如下所示。### 圖 1![](https://raw.githubusercontent.com/studygolang/gctt-images/master/lang-mechanics/81_figure1.png)你可以看到圖 1 中,當調用完 `createUserV1` ,一個 `user` 值同時存在(兩個函數的)棧幀中。在函數版本 2 中,返回指標。### 清單 3```27 func createUserV2() *user {28 u := user{29 name: "Bill",30 email: "bill@ardanlabs.com",31 }3233 println("V2", &u)34 return &u35 }```我說這個函數返回的是指標是因為這個被函數建立的 `user` 值通過調用棧被共用了。這意味著調用函數接收到一個值的地址拷貝。你可以看到在第 28 行到 31 行使用相同的欄位值來構造 `user` 值,但在第 34 行返回時卻是不同的。不是將 `user` 值的副本傳遞到調用棧,而是將 `user` 值的地址傳遞到調用棧。基於此,你也許會認為棧在調用之後是這個樣子。### 圖 2![](https://raw.githubusercontent.com/studygolang/gctt-images/master/lang-mechanics/81_figure2.png)如果看到的圖 2 真的發生的話,你將遇到一個問題。指標指向了棧下的無效地址空間。當 `main` 函數調用下一個函數,指向的記憶體將重新對應並將被重新初始化。這就是逃逸分析將開始保持完整性的地方。在這種情況下,編譯器將檢查到,在 `createUserV2` 的(函數)棧中構造 `user` 值是不安全的,因此,替代地,會在堆中構造(相應的)值。這(個分析並處理的過程)將在第 28 行構造時立即發生。## 可讀性(Readability)在上一篇博文中,我們知道一個函數只能直接存取它的(函數棧)空間,或者通過(函數棧空間內的)指標,通過跳轉訪問(函數棧空間外的)外部記憶體。這意味著訪問逃逸到堆上的值也需要通過指標跳轉。記住 `createUserV2` 的代碼的樣子:### 清單 4```27 func createUserV2() *user {28 u := user{29 name: "Bill",30 email: "bill@ardanlabs.com",31 }3233 println("V2", &u)34 return &u35 }```文法隱藏了代碼中真正發生的事情。第 28 行聲明的變數 `u` 代表一個 `user` 類型的值。Go 代碼中的類型構造不會告訴你值在記憶體中的位置。所以直到第 34 行傳回型別時,你才知道值需要逃逸(處理)。這意味著,雖然 `u` 代表類型 `user` 的一個值,但對該值的訪問必須通過指標進行。你可以在函數調用之後,看到堆棧就像(圖 3)這樣。### 圖 3![](https://raw.githubusercontent.com/studygolang/gctt-images/master/lang-mechanics/81_figure3.png)在 `createUserV2` 函數棧中,變數 `u` 代表的值存在於堆中,而不是棧。這意味著用 `u` 訪問值時,使用指標訪問而不是直接存取。你可能想,為什麼不讓 `u` 成為指標,畢竟訪問它代表的值需要使用指標?### 清單 5```27 func createUserV2() *user {28 u := &user{29 name: "Bill",30 email: "bill@ardanlabs.com",31 }3233 println("V2", u)34 return u35 }```如果你這樣做,將使你的代碼缺乏重要的可讀性。(讓我們)離開整個函數一秒,只關注 `return`。### 清單 6```34 return u35 }```這個 `return` 告訴你什麼了呢?它說明了返回 `u` 值的副本給調用棧。然而,當你使用 `&` 操作符,`return` 又告訴你什麼了呢?### 清單 7```34 return &u35 }```多虧了 `&` 操作符,`return` 告訴你 `u` 被分享給調用者,因此,已經逃逸到堆中。記住,當你讀代碼的時候,指標是為了共用,`&` 操作符對應單詞 "sharing"。這在提高可讀性的時候非常有用,這(也)是你不想失去的部分。### 清單 8```01 var u *user02 err := json.Unmarshal([]byte(r), &u)03 return u, err```為了讓其可以工作,你一定要通過共用指標變數(的方式)給(函數) `json.Unmarshal`。`json.Unmarshal` 調用時會建立 `user` 值並將其地址賦值給指標變數。https://play.golang.org/p/koI8EjpeIx代碼解釋:01:建立一個類型為 `user`,值為空白的指標。 02:跟函數 `json.Unmarshal` 函數共用指標。 03:返回 `u` 的副本給調用者。這裡並不是很好理解,`user`值被 `json.Unmarshal` 函數建立,並被共用給調用者。如何在構造過程中使用文法語義來改變可讀性?### 清單 9```01 var u user02 err := json.Unmarshal([]byte(r), &u)03 return &u, err```代碼解釋:01:建立一個類型為 `user`,值為空白的變數。 02:跟函數 `json.Unmarshal` 函數共用 `u`。 03:跟調用者共用 `u`。這裡非常好理解。第 02 行共用 `user` 值到調用棧中的 `json.Unmarshal`,在第 03 行 `user` 值共用給調用者。這個共用過程將會導致 `user` 值逃逸。在構建一個值時,使用值語義,並利用 `&` 操作符的可讀性來明確值是如何被共用的。## 編譯器報告(Compiler Reporting)想查看編譯器(關於逃逸分析)的決定,你可以讓編譯器提供一份報告。你只需要在調用 `go build` 的時候,開啟 `-gcflags` 開關,並帶上 `-m` 選項。實際上總共可以使用 4 個 `-m`,(但)超過 2 個層級的資訊就已經太多了。我將使用 2 個 `-m` 的層級。### 清單 10```shell$ go build -gcflags "-m -m"./main.go:16: cannot inline createUserV1: marked go:noinline./main.go:27: cannot inline createUserV2: marked go:noinline./main.go:8: cannot inline main: non-leaf function./main.go:22: createUserV1 &u does not escape./main.go:34: &u escapes to heap./main.go:34: from ~r0 (return) at ./main.go:34./main.go:31: moved to heap: u./main.go:33: createUserV2 &u does not escape./main.go:12: main &u1 does not escape./main.go:12: main &u2 does not escape```你可以看到編譯器報告是否需要逃逸處理的決定。編譯器都說了什麼呢?請再看一下引用的 `createUserV1` 和 `createUserV2` 函數。### 清單 13```16 func createUserV1() user {17 u := user{18 name: "Bill",19 email: "bill@ardanlabs.com",20 }2122 println("V1", &u)23 return u24 }27 func createUserV2() *user {28 u := user{29 name: "Bill",30 email: "bill@ardanlabs.com",31 }3233 println("V2", &u)34 return &u35 }```從報告中的這一行開始。### 清單 14```./main.go:22: createUserV1 &u does not escape```這是說在函數 `createUserV1` 調用 `println` 不會造成 `user` 值逃逸到堆。這是必須檢查的,因為它將會跟函數 `println` 共用(`u`)。接下來看報告中的這幾行。### 清單 15```shell./main.go:34: &u escapes to heap./main.go:34: from ~r0 (return) at ./main.go:34./main.go:31: moved to heap: u./main.go:33: createUserV2 &u does not escape```這幾行是說,類型為 `user`,並在第 31 行被賦值的 `u` 的值,因為第 34 行的 `return` 逃逸。最後一行是說,跟之前一樣,在 33 行調用 `println` 不會造成 `user` 值逃逸。閱讀這些報告可能讓人感到困惑,(編譯器)會根據所討論的變數的類型是基於實值型別還是指標類型而略有變化。將 `u` 改為指標類型的 `*user`,而不是之前的命名類型 `user`。### 清單 16```27 func createUserV2() *user {28 u := &user{29 name: "Bill",30 email: "bill@ardanlabs.com",31 }3233 println("V2", u)34 return u35 }```再次產生報告。### 清單 17```shell./main.go:30: &user literal escapes to heap./main.go:30: from u (assigned) at ./main.go:28./main.go:30: from ~r0 (return) at ./main.go:34```現在報告說在 28 行賦值的指標類型 `*user`,`u` 引用的 `user` 值,因為 34 行的 `return` 逃逸。## 結論值在構建時並不能決定它將存在於哪裡。只有當一個值被共用,編譯器才能決定如何處理這個值。當你在調用時,共用了棧上的一個值時,它就會逃逸。在下一篇中你將探索一個值逃逸的其他原因。這些文章試圖引導你選擇給定類型的值或指標的指導原則。每種方式都有(對應的)好處和(額外的)開銷。保持在棧上的值,減少了 GC 的壓力。但是需要儲存,跟蹤和維護不同的副本。將值放在堆上的指標,會增加 GC 的壓力。然而,也有它的好處,只有一個值需要儲存,跟蹤和維護。(其實,)最關鍵的是如何保持正確地、一致地以及均衡(開銷)地使用。

via: https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html

作者:William Kennedy 譯者:gogeof 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

1737 次點擊  ∙  1 贊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.