Go語言的逃逸分析機(jī)制

閱讀前請悉知:本文是一篇翻譯文章,出于對原文的喜愛與敬畏,所以需要強(qiáng)調(diào):如果讀者英文閱讀能力好,請直接移步文末原文鏈接;如果對這篇翻譯所述知識感興趣,也請一定要再看下英文原文,加深理解。翻譯中為了表達(dá)的需要,加入了自己的一些理解,不過因為知識有限,翻譯過程難免紕漏,如有問題,歡迎留言指正。

前言

在這個由四部分組成的系列的第一篇文章中,我使用了一個例子來介紹指針機(jī)制的基礎(chǔ)知識,在這個例子中,一個值被共享到goroutine的棧中。我沒有向你們展示的是當(dāng)你在棧上共享一個值時會發(fā)生什么。要理解這一點(diǎn),你需要了解另一個內(nèi)存區(qū)域:。有了這些知識,你就可以開始學(xué)習(xí)逃逸分析了。

逃逸分析是編譯器用來確定由程序創(chuàng)建的值所處位置的過程。具體來說,編譯器執(zhí)行靜態(tài)代碼分析,以確定是否可以將值放在構(gòu)造函數(shù)的棧(幀)上,或者該值是否必須“逃逸”到堆上。在Go中,沒有關(guān)鍵字或函數(shù)可以用于在此決策中指導(dǎo)編譯器。只有通過你寫的代碼來分析這一點(diǎn)。

堆是除棧之外的第二個內(nèi)存區(qū)域,用于存儲值。堆不像棧那樣是自清理的,因此使用這個內(nèi)存的成本更大。首先,成本與垃圾收集器(GC)有關(guān),垃圾收集器必須參與進(jìn)來以保持該區(qū)域的清潔。當(dāng)GC運(yùn)行時,它將使用25%的可用CPU資源。此外,它可能會產(chǎn)生微秒級的“stop the world”延遲。擁有GC的好處是你不需要擔(dān)心內(nèi)存的管理問題,因為內(nèi)存管理是相當(dāng)復(fù)雜、也容易出錯的。

堆上的值構(gòu)成Go中的內(nèi)存分配。這些分配對GC造成壓力,因為堆中不再被指針引用的每個值都需要刪除。需要檢查和刪除的值越多,GC每次運(yùn)行時必須執(zhí)行的工作就越多。因此,GC算法一直在努力在堆的大小分配和運(yùn)行速度之間尋求平衡。

共享棧

在Go中,不允許goroutine擁有指向另一個goroutine棧上的內(nèi)存的指針。這是因為當(dāng)棧必須增長或收縮時,goroutine的棧內(nèi)存可能被一個新的內(nèi)存塊替換。如果運(yùn)行時必須跟蹤指向其他goroutine棧的指針,那么管理起來就太困難了,而在這些上更新指針的“stop the world”延遲將會非常困難。

下面是一個由于增長而多次被替換的棧示例。查看第2行和第6行的輸出。你將在main的棧(幀)中看到字符串值的地址更改了兩次。(字符串s的內(nèi)存地址本來應(yīng)該是在main的幀內(nèi)的,為何會發(fā)生這種變化呢?沒搞懂)

// Sample program to show how stacks grow/change.
package main

// Number of elements to grow each stack frame.
// Run with 10 and then with 1024
const size = 1024

// main is the entry point for the application.
func main() {
    s := "HELLO"
    stackCopy(&s, 0, [size]int{})
}

// stackCopy recursively runs increasing the size
// of the stack.
func stackCopy(s *string, c int, a [size]int) {
    println(c, s, *s)

    c++
    if c == 10 {
        return
    }

    stackCopy(s, c, a)
}

輸出

0 0x1044dfa0 HELLO
1 0x1044dfa0 HELLO
2 0x10455fa0 HELLO
3 0x10455fa0 HELLO
4 0x10455fa0 HELLO
5 0x10455fa0 HELLO
6 0x10465fa0 HELLO
7 0x10465fa0 HELLO
8 0x10465fa0 HELLO
9 0x10465fa0 HELLO

逃逸機(jī)制

在函數(shù)的棧(幀)之外共享一個值時,它將被放置(或分配)在堆上。逃逸分析算法的工作是找到這些情況,并在程序中保持一定的完整性。完整性在于確保對任何值的訪問總是準(zhǔn)確、一致和高效的。

Listing 1

01 package main
02
03 type user struct {
04     name  string
05     email string
06 }
07
08 func main() {
09     u1 := createUserV1()
10     u2 := createUserV2()
11
12     println("u1", &u1, "u2", &u2)
13 }
14
15 //go:noinline
16 func createUserV1() user {
17     u := user{
18         name:  "Bill",
19         email: "bill@ardanlabs.com",
20     }
21
22     println("V1", &u)
23     return u
24 }
25
26 //go:noinline
27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

我正在使用go:noinline指令,以防止編譯器直接內(nèi)聯(lián)這些函數(shù)的代碼。內(nèi)聯(lián)將刪除函數(shù)調(diào)用并使這個示例復(fù)雜化。我將在下一篇文章中介紹內(nèi)聯(lián)的副作用。

在清單1中,你將看到一個具有兩個不同函數(shù)的程序,它們創(chuàng)建用戶值并將值返回給調(diào)用者。createUserV1在返回時使用了值語義。

Listing 2

16 func createUserV1() user {
17     u := user{
18         name:  "Bill",
19         email: "bill@ardanlabs.com",
20     }
21
22     println("V1", &u)
23     return u
24 }

我說過函數(shù)在返回時使用值語義,因為這個函數(shù)創(chuàng)建的用戶值正在被復(fù)制并傳遞給調(diào)用棧。這意味著調(diào)用函數(shù)正在接收值本身的副本。

你可以看到在第17行到第20行執(zhí)行了用戶值的構(gòu)造。然后在第23行,用戶值的副本被傳遞到調(diào)用棧并返回給調(diào)用者。函數(shù)返回后,棧是這樣的。

Figure 1

image.png

在圖1中可以看到,在調(diào)用createUserV1之后,兩個幀中都存在一個用戶值。在函數(shù)的createUserV2中,在返回時使用指針語義。

Listing 3

27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

我說過,函數(shù)在返回時使用指針語義,因為這個函數(shù)創(chuàng)建的用戶值在調(diào)用棧中被共享。這意味著調(diào)用函數(shù)正在接收該值的地址副本。

你可以看到,在第28到31行中使用了相同的struct文字構(gòu)造用戶值,但是在第34行中返回的值不同。不是將用戶值的副本傳遞回調(diào)用棧,而是傳遞用戶值的地址副本?;诖?,你可能認(rèn)為調(diào)用之后棧是這樣的。

Figure 2

image.png

如果你在圖2中看到的真的發(fā)生了,那么你就會遇到完整性問題。指針向下指向不再有效的調(diào)用棧。在main的下一個函數(shù)調(diào)用中,所指向的內(nèi)存將被重新構(gòu)造并重新初始化。

這就是逃逸分析開始維護(hù)完整性的地方。在這種情況下,編譯器將確定在createUserV2的棧楨內(nèi)構(gòu)造用戶值是不安全的,因此它將在堆上構(gòu)造值。這將由第28行初始化完成。

可讀性

正如你在上一篇文章中學(xué)到的,在所屬幀內(nèi),函數(shù)可以通過指針直接訪問楨內(nèi)的內(nèi)存,但是訪問幀外的內(nèi)存需要間接訪問。這意味著對轉(zhuǎn)義到堆的值的訪問也必須通過指針間接完成。

記住createUserV2的代碼是什么樣子的。

Listing 4

27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

語法隱藏了代碼中真正發(fā)生的事情。第28行聲明的變量u表示user類型的值。Go中的構(gòu)造不會告訴你一個值在內(nèi)存中的位置,所以直到第34行上的return語句,你才知道這個值需要逃逸。這意味著,即使u表示的是user類型的值,訪問這個user值也必須通過封面下面的指針進(jìn)行。

你可以在函數(shù)調(diào)用之后將內(nèi)存布局形象化。

Figure 3

image.png

createUserV2的棧(幀)上的u變量表示堆上的值,而不是棧上的值。這意味著使用u訪問值,需要指針訪問,而不是語法建議的直接訪問。你可能會想,既然訪問它所代表的值需要使用指針,那么為什么不讓u成為指針呢?

Listing 5

27 func createUserV2() *user {
28     u := &user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", u)
34     return u
35 }

如果你這樣做,會犧牲代碼的?可讀性。暫時離開整個函數(shù),只關(guān)注返回值。

Listing 6

34     return u
35 }

這還告訴你什么?它說的是一個u的副本被傳遞到調(diào)用棧上。然而,當(dāng)你使用&操作符時,返回告訴你什么?

Listing 7

34     return &u
35 }

多虧了&運(yùn)算符,返回現(xiàn)在告訴因為u需要共享到調(diào)用棧中,由此逃逸到堆中。記住,指針是用于共享的,并在讀取代碼時替換“共享”一詞的&操作符。這在可讀性方面非常強(qiáng)大,這是你不想失去的。

下面是另一個例子,使用指針語義構(gòu)造值會損害可讀性。

Listing 8

01 var u *user
02 err := json.Unmarshal([]byte(r), &u)
03 return u, err

你必須與json共享指針變量。在第02行調(diào)用Unmarshal,讓這段代碼工作。json.Unmarshal調(diào)用將創(chuàng)建用戶值并將其地址分配給指針變量。

這段代碼說了什么:
01:創(chuàng)建一個類型為user的指針變量。
02:與json.Unmarshal函數(shù)共享u。
03:給調(diào)用方返回u的副本。

user的值是否由json.Unmarshal函數(shù)創(chuàng)建并與調(diào)用者共享尚不清楚。

在構(gòu)造過程中使用值語義時可讀性如何變化?

Listing 9

01 var u user
02 err := json.Unmarshal([]byte(r), &u)
03 return &u, err

這段代碼說了什么:
01:創(chuàng)建一個類型為user的指針變量。
02:與json.Unmarshal函數(shù)共享u。
03:與調(diào)用方共享u。

一切都很清楚。第02行將調(diào)用棧中的user值共享給json.Unmarshal函數(shù)以及第03行將user值從調(diào)用棧上共享給調(diào)用者。此共享將導(dǎo)致用戶值轉(zhuǎn)義。

在構(gòu)造值時使用值語義,并利用&運(yùn)算符的可讀性來明確值是如何被共享的。

編譯器報告

要查看編譯器正在做出的決策,你可以要求編譯器提供一個報告。你所需要做的就是在go build調(diào)用中使用帶有-m選項的-gcflags開關(guān)。

實際上有4個級別的-m可以使用,但是超過2個級別的信息就會讓人不知所措。我將使用-m的兩個級別。

Listing 10

$ 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

你可以看到編譯器正在報告逃逸情況。編譯器在說什么?首先再次查看createUserV1createUserV2函數(shù)以供參考。

Listing 13

16 func createUserV1() user {
17     u := user{
18         name:  "Bill",
19         email: "bill@ardanlabs.com",
20     }
21
22     println("V1", &u)
23     return u
24 }

27 func createUserV2() *user {
28     u := user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", &u)
34     return &u
35 }

從報告中的這一行開始。

Listing 14

./main.go:22: createUserV1 &u does not escape

這就是說,在createUserV1函數(shù)中對println的函數(shù)調(diào)用不會導(dǎo)致用戶值轉(zhuǎn)義到堆中。必須檢查它,因為它正在與println函數(shù)共享。

接下來看看報告中的這些行。

Listing 15

./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

這些行表示,與u變量關(guān)聯(lián)的user值(它是命名類型user,在第31行分配)因為在第34行返回而轉(zhuǎn)義。最后一行和前面一樣,第33行上的println調(diào)用不會導(dǎo)致用戶值轉(zhuǎn)義。

閱讀這些報告可能會讓人感到困惑,并可能會根據(jù)所涉及的變量類型是基于命名類型還是基于文字類型而略有變化。

u更改為文字類型*user,而不是之前的命名類型user

Listing 16

27 func createUserV2() *user {
28     u := &user{
29         name:  "Bill",
30         email: "bill@ardanlabs.com",
31     }
32
33     println("V2", u)
34     return u
35 }

再回頭看報告

Listing 17

./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

現(xiàn)在,報告說,由于在第34行返回,由u變量引用的用戶值正在轉(zhuǎn)義,該變量是文本類型*user,在第28行賦值。

結(jié)論

一個值的構(gòu)造并不決定它的位置。只有如何共享一個值才能決定編譯器將如何處理該值。任何時候你在調(diào)用棧上共享一個值,它都會被轉(zhuǎn)義。在下一篇文章中,你將探討其他原因來解釋值的轉(zhuǎn)義。

這些帖子試圖引導(dǎo)你為任何給定類型選擇值或指針語義的指導(dǎo)原則。每種語義都有其優(yōu)點(diǎn)和代價。值語義將值保存在棧上,從而減少對GC的壓力。但是,任何給定值都有不同的副本,必須存儲、跟蹤和維護(hù)。指針語義將值放在堆上,這會對GC造成壓力。但是,它們是高效的,因為只有一個值需要存儲、跟蹤和維護(hù)。關(guān)鍵是正確、一致和平衡地使用每個語義。


版權(quán)聲明:

任何個人或機(jī)構(gòu)如需轉(zhuǎn)載本文,無須再獲得作者書面授權(quán),但是轉(zhuǎn)載者必須保留作者署名,并注明出處。

作者保留對本文的修改權(quán)。他人未經(jīng)作者許可,不得擅自修改,破壞作品的完整性。

作者保留對本文的其他各項著作權(quán)權(quán)利。

原文閱讀:
Language Mechanics On Escape Analysis

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 閱讀前請悉知:本文是一篇翻譯文章,出于對原文的喜愛與敬畏,所以需要強(qiáng)調(diào):如果讀者英文閱讀能力好,請直接移步文末原文...
    wu_sphinx閱讀 1,114評論 0 0
  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 14,264評論 0 38
  • 一個項目的開始,首先要確定要做的項目是什么,還要經(jīng)過市場調(diào)研,看這個項目是否有做的必要,以及自己做的項目的優(yōu)勢是什...
    eff7af6c2f06閱讀 163評論 0 2
  • 誤會我談戀愛了 從同學(xué)她媽那兒聽來的 呵呵 我和她都很久沒有聯(lián)系了好么 哈 下次這種無事生非的輿論八卦 還是一笑置...
    夢小飛閱讀 176評論 0 0

友情鏈接更多精彩內(nèi)容