C語言中文網 目錄

Go語言變量生命期,Go語言變量逃逸分析

< 上一頁Go語言指針 Go語言常量下一頁 >

討論變量生命期之前,先來了解下計算機組成里兩個非常重要的概念:堆和棧。

什么是棧

棧(Stack)是一種擁有特殊規則的線性表數據結構。

1) 概念

棧只允許往線性表的一端放入數據,之后在這一端取出數據,按照后進先出(LIFO,Last InFirst Out)的順序,如下圖所示。


圖:棧的操作及擴展

往棧中放入元素的過程叫做入棧。入棧會增加棧的元素數量,最后放入的元素總是位于棧的頂部,最先放入的元素總是位于棧的底部。

從棧中取出元素時,只能從棧頂部取出。取出元素后,棧的數量會變少。最先放入的元素總是最后被取出,最后放入的元素總是最先被取出。不允許從棧底獲取數據,也不允許對棧成員(除棧頂外的成員)進行任何查看和修改操作。

棧的原理類似于將書籍一本一本地堆起來。書按順序一本一本從頂部放入,要取書時只能從頂部一本一本取出。

2) 變量和棧有什么關系

棧可用于內存分配,棧的分配和回收速度非常快。下面代碼展示棧在內存分配上的作用,代碼如下:
func calc(a, b int) int {
    var c int
    c = a * b

    var x int
    x = c * 10

    return x
}
代碼說明如下:
  • 第 1 行,傳入 a、b 兩個整型參數。
  • 第 2 行,聲明 c 整型變量,運行時,c 會分配一段內存用以存儲 c 的數值。
  • 第 3 行,將 a 和 b 相乘后賦予 c。
  • 第 5 行,聲明 x 整型變量,x 也會被分配一段內存。
  • 第 6 行,讓 c 乘以 10 后存儲到 x 變量中。
  • 第 8 行,返回 x 的值。

上面的代碼在沒有任何優化情況下,會進行 c 和 x 變量的分配過程。Go 語言默認情況下會將 c 和 x 分配在棧上,這兩個變量在 calc() 函數退出時就不再使用,函數結束時,保存 c 和 x 的棧內存再出棧釋放內存,整個分配內存的過程通過棧的分配和回收都會非常迅速。

什么是堆

堆在內存分配中類似于往一個房間里擺放各種家具,家具的尺寸有大有小。分配內存時,需要找一塊足夠裝下家具的空間再擺放家具。經過反復擺放和騰空家具后,房間里的空間會變得亂七八糟,此時再往空間里擺放家具會存在雖然有足夠的空間,但各空間分布在不同的區域,無法有一段連續的空間來擺放家具的問題。此時,內存分配器就需要對這些空間進行調整優化,如下圖所示。


圖:堆的分配及空間

堆分配內存和棧分配內存相比,堆適合不可預知大小的內存分配。但是為此付出的代價是分配速度較慢,而且會形成內存碎片。

變量逃逸(Escape Analysis)——自動決定變量分配方式,提高運行效率

堆和棧各有優缺點,該怎么在編程中處理這個問題呢?在 C/C++ 語言中,需要開發者自己學習如何進行內存分配,選用怎樣的內存分配方式來適應不同的算法需求。比如,函數局部變量盡量使用棧;全局變量、結構體成員使用堆分配等。程序員不得不花費很多年的時間在不同的項目中學習、記憶這些概念并加以實踐和使用。

Go 語言將這個過程整合到編譯器中,命名為“變量逃逸分析”。這個技術由編譯器分析代碼的特征和代碼生命期,決定應該如何堆還是棧進行內存分配,即使程序員使用 Go 語言完成了整個工程后也不會感受到這個過程。

1) 逃逸分析

使用下面的代碼來展現 Go 語言如何通過命令行分析變量逃逸,代碼如下:
package main

import "fmt"

// 本函數測試入口參數和返回值情況
func dummy(b int) int {

    // 聲明一個c賦值進入參數并返回
    var c int
    c = b

    return c
}

// 空函數, 什么也不做
func void() {

}

func main() {

    // 聲明a變量并打印
    var a int

    // 調用void()函數
    void()

    // 打印a變量的值和dummy()函數返回
    fmt.Println(a, dummy(0))
}
代碼說明如下:
  • 第 6 行,dummy() 函數擁有一個參數,返回一個整型值,測試函數參數和返回值分析情況。
  • 第 9 行,聲明 c 變量,這里演示函數臨時變量通過函數返回值返回后的情況。
  • 第 16 行,這是一個空函數,測試沒有任何參數函數的分析情況。
  • 第 23 行,在 main() 中聲明 a 變量,測試 main() 中變量的分析情況。
  • 第 26 行,調用 void() 函數,沒有返回值,測試 void() 調用后的分析情況。
  • 第 29 行,打印 a 和 dummy(0) 的返回值,測試函數返回值沒有變量接收時的分析情況。

接著使用如下命令行運行上面的代碼:

$ go run -gcflags "-m -l" main.go

使用 go run 運行程序時,-gcflags 參數是編譯參數。其中 -m 表示進行內存分配分析,-l 表示避免程序內聯,也就是避免進行程序優化。

運行結果如下:
# command-line-arguments
./main.go:29:13: a escapes to heap
./main.go:29:22: dummy(0) escapes to heap
./main.go:29:13: main ... argument does not escape
0 0

程序運行結果分析如下:
  • 輸出第 2 行告知“main 的第 29 行的變量 a 逃逸到堆”。
  • 第 3 行告知“dummy(0)調用逃逸到堆”。由于 dummy() 函數會返回一個整型值,這個值被 fmt.Println 使用后還是會在其聲明后繼續在 main() 函數中存在。
  • 第 4 行,這句提示是默認的,可以忽略。

上面例子中變量 c 是整型,其值通過 dummy() 的返回值“逃出”了 dummy() 函數。c 變量值被復制并作為 dummy() 函數返回值返回,即使 c 變量在 dummy() 函數中分配的內存被釋放,也不會影響 main() 中使用 dummy() 返回的值。c 變量使用棧分配不會影響結果。

2) 取地址發生逃逸

下面的例子使用結構體做數據,了解在堆上分配的情況,代碼如下:
package main

import "fmt"

// 聲明空結構體測試結構體逃逸情況
type Data struct {
}

func dummy() *Data {

    // 實例化c為Data類型
    var c Data

    //返回函數局部變量地址
    return &c
}

func main() {

    fmt.Println(dummy())
}
代碼說明如下:
  • 第 6 行,聲明一個空的結構體做結構體逃逸分析。
  • 第 9 行,將 dummy() 函數的返回值修改為 *Data 指針類型。
  • 第 12 行,將 c 變量聲明為 Data 類型,此時 c 的結構體為值類型。
  • 第 15 行,取函數局部變量 c 的地址并返回。Go 語言的特性允許這樣做。
  • 第 20 行,打印 dummy() 函數的返回值。

執行逃逸分析:
$ go run -gcflags "-m -l" main.go
# command-line-arguments
./main.go:15:9: &c escapes to heap
./main.go:12:6: moved to heap: c
./main.go:20:19: dummy() escapes to heap
./main.go:20:13: main ... argument does not escape
&{}

注意第 4 行出現了新的提示:將 c 移到堆中。這句話表示,Go 編譯器已經確認如果將 c 變量分配在棧上是無法保證程序最終結果的。如果堅持這樣做,dummy() 的返回值將是 Data 結構的一個不可預知的內存地址。這種情況一般是 C/C++ 語言中容易犯錯的地方:引用了一個函數局部變量的地址。

Go 語言最終選擇將 c 的 Data 結構分配在堆上。然后由垃圾回收器去回收 c 的內存。

3) 原則

在使用 Go 語言進行編程時,Go 語言的設計者不希望開發者將精力放在內存應該分配在棧還是堆上的問題。編譯器會自動幫助開發者完成這個糾結的選擇。但變量逃逸分析也是需要了解的一個編譯器技術,這個技術不僅用于 Go 語言,在 Java 等語言的編譯器優化上也使用了類似的技術。

編譯器覺得變量應該分配在堆和棧上的原則是:
  • 變量是否被取地址。
  • 變量是否發生逃逸。
< 上一頁Go語言指針 Go語言常量下一頁 >

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo