亚洲熟女综合色一区二区三区,亚洲精品中文字幕无码蜜桃,亚洲va欧美va日韩va成人网,亚洲av无码国产一区二区三区,亚洲精品无码久久久久久久

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

本文作者記錄了 Go 變量分配位置的觀測(cè)技巧和 Go 語(yǔ)言設(shè)計(jì)思路。

一、由 iter 包引發(fā)的疑問(wèn)

最近在 GitHub 上偶然發(fā)現(xiàn)了 Brad Fitzpatrick 的 iter 包,整個(gè)包只有 一個(gè)函數(shù)(一行代碼):

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

但其中的一行注釋令人費(fèi)解:

It does not cause any allocations.

1. 空結(jié)構(gòu)體

我們知道,struct{} 是空結(jié)構(gòu)體(empty struct)。關(guān)于空結(jié)構(gòu)體,Dave Cheney 在 The empty struct 中作了很好地闡述:

  • 空結(jié)構(gòu)體不占用空間(The empty struct consumes no storage)。
  • 空結(jié)構(gòu)體的切片只占用切片頭的空間(Slices of struct{}s consume only the space for their slice header)。

2. Go 切片

按照官方博客 Go Slices: usage and internals 的說(shuō)法:

A slice is a descriptor of an array segment. It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).

因?yàn)榍衅偸侵赶蛞粋€(gè)底層數(shù)組的,所以所謂的 “切片頭” 其實(shí)就是切片本身。一個(gè)切片包括:指向數(shù)組片段的指針、數(shù)組片段的長(zhǎng)度和最大長(zhǎng)度,總共 3 個(gè)字長(zhǎng)(在 64 位機(jī)器上,就是 24 個(gè)字節(jié))。

3. 疑問(wèn)

按照上面的分析,在 64 位機(jī)器上,不管 n 是多少,make([]struct{}, n)?得到的切片一定會(huì)占用 24 個(gè)字節(jié),reddit 上的討論 也證實(shí)了我們的分析。

那為什么 Brad Fitzpatrick 聲稱函數(shù) N 不會(huì)引發(fā)分配呢?

為了解決這個(gè)疑惑,我們需要先弄清楚兩個(gè)問(wèn)題:

  1. 一個(gè) Go 變量可能會(huì)被分配在哪里?
  2. 如何確定一個(gè) Go 變量最終會(huì)被分配在哪里?

二、Go 變量可能的分配位置

1. 進(jìn)程的內(nèi)存布局

在 Linux/x86-32 系統(tǒng)中,一個(gè)進(jìn)程的典型的內(nèi)存布局如下圖所示(圖片來(lái)自 The Linux Programming Interface 圖 6-1):

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

結(jié)合維基百科對(duì) Data segment 的描述,我們得知:

  • 初始化的全局變量或靜態(tài)變量,會(huì)被分配在 Data 段。
  • 未初始化的全局變量或靜態(tài)變量,會(huì)被分配在 BSS 段。
  • 在函數(shù)中定義的局部變量,會(huì)被分配在堆(Heap 段)或棧(Stack 段)。
    • 實(shí)際上,如果考慮到 編譯器優(yōu)化,局部變量還可能會(huì)被 分配在寄存器,或者直接被 優(yōu)化去掉。

2. Go 內(nèi)存分配

對(duì)于 Go 而言,有兩個(gè)地方可以用于分配:

  • 堆(heap)
    • 由 GC 負(fù)責(zé)回收。
    • 對(duì)應(yīng)于進(jìn)程地址空間的堆。
  • 棧(stack)
    • 不涉及 GC 操作。
    • 每個(gè) goroutine 都有自己的棧,初始時(shí)被分配在進(jìn)程地址空間的棧上,擴(kuò)容時(shí)被分配在進(jìn)程地址空間的堆上。

Go 變量主要分為兩種:

  • 全局變量
    • 會(huì)被 Go 編譯器標(biāo)記為一些特殊的 符號(hào)類型,分配在堆上還是棧上目前尚不清楚,不過(guò)不是本文討論的重點(diǎn)。
  • 局部變量

所以綜上,對(duì)于在函數(shù)中定義的?Go 局部變量:要么被分配在堆上,要么被分配在棧上

三、確定 Go 變量最終的分配位置

至此,我們還剩下一個(gè)問(wèn)題:對(duì)于一個(gè) Go 局部變量,如何確定它被分配在堆上還是棧上?

按照官方 FAQ How do I know whether a variable is allocated on the heap or the stack? 的解釋:

  • Go 編譯器會(huì)盡可能將變量分配在棧上
  • 以下兩種情況,Go 編譯器會(huì)將變量分配在堆上
    • 如果一個(gè)變量被取地址(has its address taken),并且被逃逸分析(escape analysis)識(shí)別為 “逃逸到堆”(escapes to heap)
    • 如果一個(gè)變量很大(very large)

1. 逃逸分析

以使用 iter 包的這段代碼為例:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

下列演示中,我將使用 Go 1.11.4:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

下面我們對(duì)這段代碼作逃逸分析:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

按照前面的分析,從 “make([]struct {}, iter.n) escapes to heap” 的信息,我們推斷:make([]struct {}, iter.n)?會(huì)被分配在堆上。

到這里,我們最初的疑惑似乎已經(jīng)有了答案:make([]struct {}, iter.n)?一定會(huì)引發(fā)堆分配,那是 Brad Fitzpatrick 的注釋寫(xiě)錯(cuò)了嗎?

2. 內(nèi)存分配器追蹤

除了逃逸分析,Go 還提供了一種叫內(nèi)存分配器追蹤(Memory Allocator Trace)的方法,用于細(xì)粒度地分析由程序引發(fā)的所有堆分配(和釋放)操作:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

因?yàn)檫M(jìn)行內(nèi)存分配器追蹤時(shí),很多由 runtime 引發(fā)的分配信息也會(huì)被打印出來(lái),所以我們用 grep 進(jìn)行過(guò)濾,只顯示由用戶代碼(user code)引發(fā)的分配信息。然而這里的輸出結(jié)果為空,表明?make([]struct {}, iter.n)?沒(méi)有引發(fā)任何堆分配。

內(nèi)存分配器追蹤的結(jié)論與逃逸分析的結(jié)論截然相反!那到底哪個(gè)結(jié)論是對(duì)的呢?

3. 匯編分析

黔驢技窮之際,Go’s Memory Allocator - Overview 這篇文章給了我提示:

So, we know that i is going to be allocated on the heap. But how does the runtime set that up? With the compiler’s help!?We can get an idea from reading the generated assembly.

關(guān)于 Go 匯編(assembly),推薦大家閱讀 Go internals, Chapter 1: Go assembly。

下面我們來(lái)看看示例代碼對(duì)應(yīng)的匯編:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

可以看到,其中有一處對(duì)?runtime.makeslice(SB)?的調(diào)用,顯然是由?make([]struct{}, n)?引發(fā)的。

查看 runtime.makeslice 的源碼:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

其中,mallocgc 的源碼如下:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

slice 對(duì)應(yīng)的結(jié)構(gòu)體如下:

如何確定一個(gè) Go 變量會(huì)被分配在哪里?

結(jié)合上述幾段源碼,我們可以看出:

  • makeslice 函數(shù)中:slice 結(jié)構(gòu)體正是我們?cè)诘谝还?jié)提到的 Go 切片 —— array 是指向數(shù)組片段的指針,len 是數(shù)組片段的長(zhǎng)度,cap 是數(shù)組片段的最大長(zhǎng)度。
  • makeslice 函數(shù)中:array 的值來(lái)自 p,而 p 則是一個(gè)指針,它指向由 mallocgc 分配得到的底層數(shù)組。
  • mallocgc 函數(shù)中:因?yàn)榭战Y(jié)構(gòu)體的 size 為 0,所以 mallocgc 并沒(méi)有實(shí)際進(jìn)行堆分配;由于沒(méi)有執(zhí)行到 tracealloc 的地方,所以進(jìn)行內(nèi)存分配器追蹤時(shí),不會(huì)采集到相關(guān)的分配信息。
  • makeslice 函數(shù)中:切片 slice 本身是以結(jié)構(gòu)體的形式返回的,所以只會(huì)被分配在棧上。

四、總結(jié)

經(jīng)過(guò)一系列的探索和分析,至此,我們可以得出以下結(jié)論:

  • make([]struct{}, n)?只會(huì)被分配在棧上,而不會(huì)被分配在堆上。
  • Brad Fitzpatrick 的注釋是對(duì)的,并且他的意思是 “不會(huì)引發(fā)堆分配”。
  • 逃逸分析識(shí)別出 escapes to heap,并不一定就是堆分配,也可能是棧分配。
  • 進(jìn)行內(nèi)存分配器追蹤時(shí),如果采集不到堆分配信息,那一定只有棧分配。

最后,我們來(lái)解答文章標(biāo)題提出的疑問(wèn) —— 如何確定一個(gè) Go 變量會(huì)被分配在哪里?對(duì)此,我們的答案是:

  1. 先對(duì)代碼作逃逸分析
    • 如果該變量被識(shí)別為 escapes to heap,那么它十有八九是被分配在堆上。
    • 如果該變量被識(shí)別為 does not escape,或者沒(méi)有與之相關(guān)的分析結(jié)果,那么它一定是被分配在棧上。
  2. 如果對(duì) escapes to heap 心存疑惑,就對(duì)代碼作內(nèi)存分配器追蹤
    • 如果有采集到與該變量相關(guān)的分配信息,那么它一定是被分配在堆上。
    • 否則,該變量一定是被分配在棧上。
  3. 此外,如果想知道 Go 編譯器是如何將變量分配在堆上或者棧上的,可以去分析 Go 匯編(以及 runtime 源碼)。

五、思考題

  • 如果換成?make([]int, n),結(jié)果還會(huì)是棧分配嗎?
  • 如果換成?make([]int, 4)?呢?
  • 除了空結(jié)構(gòu)體?make([]struct{}, n)?的特例,還有哪些 “被逃逸分析識(shí)別為 escapes to heap,但其實(shí)是棧分配” 的案例?
  • Go 支持閉包(closure),那么閉包中的變量,又是分配在哪里的?(Where are variables in a closure stored - stack or heap? 說(shuō)是分配在棧上,對(duì)于 Go 也是成立的嗎?)

轉(zhuǎn)自:russellluo.com/2019/07/how-to-confirm-where-a-go-variable-will-be-allocated.html

相關(guān)新聞

歷經(jīng)多年發(fā)展,已成為國(guó)內(nèi)好評(píng)如潮的Linux云計(jì)算運(yùn)維、SRE、Devops、網(wǎng)絡(luò)安全、云原生、Go、Python開(kāi)發(fā)專業(yè)人才培訓(xùn)機(jī)構(gòu)!