聊一聊堆、棧與Go語言的指針

堆、棧與指針

前言

堆、棧在計算機領域是亙古不變的熱門話題,歸根結底它們和編程語言無關,都是操做系統層面的內存劃分,後面嘗試簡單地拆開這幾個概念,談談我對它們的理解。html

每一個函數中每一個值在棧中都是獨佔的,不能在其餘棧中被訪問。每一個方法片(function frame)都有一個本身的獨享棧,這個棧的生命週期隨着方法開始結束誕生與消逝,在方法結束時候會被釋放掉,較之於堆,棧的優點是比較輕量級,隨用隨棄,存活期跟隨着函數。golang

通俗的講,假如說棧是各個函數的一棟私人住宅,堆就是一個大型的人民廣場,它能夠被共享。堆做爲一個全局訪問塊,它的空間由GC(拆遷大隊)管理做。編程

The heap is not self cleaning like stacks, so there is a bigger cost to using this memory. Primarily, the costs are associated with the garbage collector (GC), which must get involved to keep this area clean.數組

翻譯過來,區別於棧在函數調用結束時候就釋放掉,堆不會自動釋放,堆空間的釋放主要來自於垃圾回收操做。編程語言

GC(Garbage collection)

垃圾回收, 垃圾回收具備多種策略,通常來講,每個存在於堆中,但再也不被指針所引用的變量,都會被回收掉。「These allocations put pressure on the GC because every value on the heap that is no longer referenced by a pointer, needs to be removed.」因爲垃圾回收涉及內存操做,每每須要考慮許多因素,所幸通過漫長演變,前人種樹,有些編程語言垃圾回收策略已經足夠強大(Java,Go),咱們大部分時候不須要去幹涉內存清理,把這部分工做交給底層調度器。ide

分配到堆內存有個弊端是它會爲下一次GC增長壓力,好處是能夠被其餘棧所共享,下列情景編譯器會傾向於將它放在堆中存儲:函數

  • 嘗試申請一個較大的結構體/數組
  • 變量在必定的時間內還會被使用
  • 編譯期間不能確認大小的變量申請

指針

指針,本質上和其餘類型同樣,只不過它的值是內存地址(引用),個人理解是內存塊的門牌號。有句常常被說起的話:工具

何時該使用指針取決於何時要分享它。
Pointers serve one purpose, to share a value with a function so the function can read and write to that value even though the value does not exist directly inside its own frame.性能

指針是爲了讓變量在不一樣函數方法塊(棧區間)之間分享,而且提供變量讀寫操做。優化


結合上述的理解,指針指的是內存地址,堆是共享模塊,指針是爲了共享同一塊內存片。在Go語言中,全部傳參都是值傳遞,指針也是經過傳遞指針的值。

程序實例 指針共享

主函數

func main() {
	var v int = 1
	fmt.Printf("# Main frame: Value of v:\t\t %v, address: %p\n", v, &v)
	PassValue(v, &v)
	fmt.Printf("# Main frame: Value of v:\t\t %v, address: %p\n", v, &v)
}
複製代碼

子函數

func PassValue(fv int, addV *int) {
	// fv 的地址只屬於該函數, 由該函數棧分配
	fmt.Printf("# Func frame: Value of fv:\t\t %v, address: %p\n", fv, &fv)
	//本次修改只在該函數生效
	fv = 0
	fmt.Printf("# Func frame: Value of fv:\t\t %v, address: %p\n", fv, &fv)

	/* * 根據main函數傳入的全局地址, 對指針執行操做外部是可見的, * 由於改指針操做的都是同一個內存塊的內容 */
	*addV++
	fmt.Printf("# Func frame: Value of addV:\t %v, address: %p\n", *addV, addV)
}
複製代碼

輸出:

# Main frame: Value of v:		 1, address: 0xc000054080
# Func frame: Value of fv:		 1, address: 0xc0000540a0
# Func frame: Value of fv:		 0, address: 0xc0000540a0
# Func frame: Value of addV:	 2, address: 0xc000054080
# Main frame: Value of v:		 2, address: 0xc000054080
複製代碼

能夠看到傳遞指針在子函數裏面操做的都是同一個地址(0xc000054080),因此在子函數退出時候對v的改變在主函數是可見的。
而傳進子函數的fv所在地址已經處於子函數的管轄棧,隨着函數結束,該棧會被釋放。


棧逃逸

  1. 訪問外部棧
    指的是變量在函數執行結束時候,沒有隨着函數棧結束生命,值超出函數(棧)的調用週期,逃到堆去了(一般是一個全局指針),能被外部所共享,能夠經過go自帶的工具來分析。下面是棧逃逸的一個栗子。

    //go:noinline
    func CreatePointer() *int  {
    	return new(int)
    }
    複製代碼

    分析

    $ go build -gcflags "-m -m -l" escape.go
    # command-line-arguments
    .\escape.go:9:12: new(int) escapes to heap
    .\escape.go:9:12:       from ~r0 (return) at .\escape.go:9:2
    複製代碼

    能夠看到提示, return new(int)這個語句把new指針返回給調用方,這個隨着CreatePointer函數結束的時候仍然有外部引用到這個指針,已經超出了該函數棧的範圍,因此編譯器提示它將分配到堆中去。

  2. 編譯未肯定
    關於棧逃逸,還有另一種情景會發生,再看一個栗子:

    func SpecifySizeAllocate()  {
    	buf := make([]byte, 5)
    	println(buf)
    }
    
    func UnSpecifySizeAllocate(size int)  {
    	buf := make([]byte, size)
    	println(buf)
    }
    複製代碼

    分析

    $ go build -gcflags "-m -m" escape.go
    # command-line-arguments
    .\escape.go:5:6: can inline SpecifySizeAllocate as: func() { buf := make([]byte, 5); println(buf) }
    .\escape.go:10:6: can inline UnSpecifySizeAllocate as: func(int) { buf := make([]byte, size); println(buf) }
    .\escape.go:6:13: SpecifySizeAllocate make([]byte, 5) does not escape
    .\escape.go:11:13: make([]byte, size) escapes to heap
    .\escape.go:11:13:      from make([]byte, size) (non-constant size) at .\escape.go:11:13
    
    複製代碼

    觀察這兩個函數,兩個buf的生存期都只在函數裏面,好像都不會逃逸,然而根據分析結果,UnSpecifySizeAllocate()這個函數卻產生了棧逃逸,這是爲何呢?
    能夠看到,分析提示「non-constant size」/「沒有具體大小」,這是由於,編譯器並不能在編譯階段知道size的值,因此無法在函數棧裏面分配確切大小的區間給buf,若是編譯器遇到這種未肯定的大小分配,會把他分配到堆中去。 這也解釋了爲何SpecifySizeAllocate()函數沒有產生逃逸。


後記:

通過時間的演變編譯器已經足夠智能,堆棧的申請分配能夠放心交給它們去作,大部分業務代碼並不須要過分考慮變量的分配,這裏僅僅是嘗試刨析程序變量在內存中的劃分,理解一些概念,要知道堆棧分析只是性能調優其中的一種方式。

固然,這裏不只僅是性能問題,在參數傳遞中,使用值拷貝仍是使用指針,最好要結合這個變量將來的做用域而決定。

Go自帶一些工具方便咱們分析底層的實現,在遵循前人的建議下,堆棧的分析可能更適合處於業務代碼完成以後的優化階段中,前期爲了保證代碼的功能和可讀性,程序猿應該首選專一於實現,當後面遇到性能瓶頸了,嘗試從堆棧分配處優化可能纔是要考慮的,畢竟有個原則叫作不要過早優化。

參考連接:

官檔:How do I know whether a variable is allocated on the heap or the stack
golang.org/doc/faq#sta…
Ardan labs 四連乾貨(推薦):
www.ardanlabs.com/blog/2017/0…
Go: Should I Use a Pointer instead of a Copy of my Struct?
medium.com/a-journey-w…
Memory : Stack vs Heap
www.gribblelab.org/CBootCamp/7…

相關文章
相關標籤/搜索