­

深入 Go 語言 defer 實現原理

轉載請聲明出處哦~,本篇文章發佈於luozhiyun的博客: //www.luozhiyun.com/archives/523

本文使用的go的源碼 1.15.7

介紹

defer 執行規則

多個defer的執行順序為”後進先出LIFO “

package main

import (  
    "fmt"
)

func main() {  
    name := "Naveen"
    fmt.Printf("Original String: %s\n", string(name))
    fmt.Printf("Reversed String: ")
    for _, v := range []rune(name) {
        defer fmt.Printf("%c", v)
    }
} 

在上面的例子中,使用 for 循環將字符串 Naveen進行遍歷後調用 defer,這些 defer調用彷彿就像被壓棧一樣,最後被推入堆棧的defer調用將被拉出並首先執行。

輸出結果如下:

$ go run main.go 
Original String: Naveen
Reversed String: neevaN

defer 聲明時會先計算確定參數的值

func a() {
    i := 0
    defer fmt.Println(i) // 0
    i++
    return
}

在這個例子中,變量 i 在 defer被調用的時候就已經確定了,而不是在 defer執行的時候,所以上面的語句輸出的是 0。

defer 可以修改有名返回值函數的返回值

如同官方所說:

For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned.

上面所說的是,如果一個被defer調用的函數是一個 function literal,也就是說是閉包或者匿名函數,並且調用 defer的函數時一個有名返回值(named result parameters)的函數,那麼 defer 可以直接訪問有名返回值並進行修改的。

例子如下:

// f returns 42
func f() (result int) {
	defer func() {
		result *= 7
	}()
	return 6
}

但是需要注意的是,只能修改有名返回值(named result parameters)函數,匿名返回值函數是無法修改的,如下:

// f returns 100
func f() int {
	i := 100
	defer func() {
		i++
	}()
	return i
}

因為匿名返回值函數是在return執行時被聲明,因此在defer語句中只能訪問有名返回值函數,而不能直接訪問匿名返回值函數。

defer 的類型

Go 在 1.13 版本 與 1.14 版本對 defer 進行了兩次優化,使得 defer 的性能開銷在大部分場景下都得到大幅降低。

堆上分配

在 Go 1.13 之前所有 defer 都是在堆上分配,該機制在編譯時:

  1. defer 語句的位置插入 runtime.deferproc,被執行時,defer 調用會保存為一個 runtime._defer 結構體,存入 Goroutine 的_defer 鏈表的最前面;
  2. 在函數返回之前的位置插入runtime.deferreturn,被執行時,會從 Goroutine 的 _defer 鏈表中取出最前面的runtime._defer 並依次執行。

棧上分配

Go 1.13 版本新加入 deferprocStack 實現了在棧上分配 defer,相比堆上分配,棧上分配在函數返回後 _defer 便得到釋放,省去了內存分配時產生的性能開銷,只需適當維護 _defer 的鏈表即可。按官方文檔的說法,這樣做提升了約 30% 左右的性能。

除了分配位置的不同,棧上分配和堆上分配並沒有本質的不同。

值得注意的是,1.13 版本中並不是所有defer都能夠在棧上分配。循環中的defer,無論是顯示的for循環,還是goto形成的隱式循環,都只能使用堆上分配,即使循環一次也是只能使用堆上分配:

func A1() {
	for i := 0; i < 1; i++ {
		defer println(i)
	}
}

$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
        ...
        0x004e 00078 (main.go:5)        CALL    runtime.deferproc(SB)
        ...
        0x005a 00090 (main.go:5)        CALL    runtime.deferreturn(SB)
        0x005f 00095 (main.go:5)        MOVQ    32(SP), BP
        0x0064 00100 (main.go:5)        ADDQ    $40, SP
        0x0068 00104 (main.go:5)        RET

開放編碼

Go 1.14 版本加入了開發編碼(open coded),該機制會defer調用直接插入函數返回之前,省去了運行時的 deferprocdeferprocStack 操作。,該優化可以將 defer 的調用開銷從 1.13 版本的 ~35ns 降低至 ~6ns 左右。

不過需要滿足一定的條件才能觸發:

  1. 沒有禁用編譯器優化,即沒有設置 -gcflags "-N"
  2. 函數內 defer 的數量不超過 8 個,且return語句與defer語句個數的乘積不超過 15;
  3. 函數的 defer 關鍵字不能在循環中執行;

defer 結構體

type _defer struct {
	siz     int32  		//參數和結果的內存大小
	started bool
	heap    bool 		//是否是堆上分配
    openDefer bool		// 是否經過開放編碼的優化
	sp        uintptr   //棧指針
	pc        uintptr   // 調用方的程序計數器
	fn        *funcval 	// 傳入的函數
	_panic    *_panic   
	link      *_defer 	//defer鏈表
	fd   unsafe.Pointer  
	varp uintptr        
	framepc uintptr
}

上面需要注意的幾個參數是 sizheapfnlinkopenDefer這些參數會在下面的分析中講到。

分析

本文分析時,先從堆上分配講起,會順帶講一下 defer 的執行規則為啥是開頭所說的那樣,然後再講講 defer 的棧上分配以及開發編碼相關內容。

分析一開始還是基於函數調用來作為入口進行分析,對函數調用還不懂的同學可以看看:《從棧上理解 Go語言函數調用 //www.luozhiyun.com/archives/518 》。

堆上分配

有名函數返回值調用

這裡我們還是以上面提到的例子作為開端,從函數調用開始研究一下堆上分配的情況。需要注意的是我在1.15 版本上面運行下面的例子並不會直接分配到堆上,需要自己去重新編譯一下 Go 源碼讓 defer 強行分配在堆上:

文件位置:src/cmd/compile/internal/gc/ssa.go

func (s *state) stmt(n *Node) {
    ...
	case ODEFER: 
		if s.hasOpenDefers {
			s.openDeferRecord(n.Left)
		} else {
			d := callDefer
            // 這裡需要注釋掉
			// if n.Esc == EscNever {
			// 	d = callDeferStack
			// }
			s.call(n.Left, d)
		}
    ...
}

如果不知道怎麼編譯的同學,可以看一下這篇:《如何編譯調試 Go runtime 源碼 //www.luozhiyun.com/archives/506

func main() {
	f()
}

func f() (result int) {
	defer func() {
		result *= 7
	}() 
	return 6
}

使用命令打印一下彙編:

$ GOOS=linux GOARCH=amd64 go tool compile -S -N -l main.go

首先看一下 main 函數,其實沒什麼講的,非常簡單的調用了一下 f 函數:

"".main STEXT size=54 args=0x0 locals=0x10
        0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $16-0
        ...
        0x0020 00032 (main.go:4)        CALL    "".f(SB)
        ...

下面分段看一下 f 函數的調用情況:

"".f STEXT size=126 args=0x8 locals=0x20
        0x0000 00000 (main.go:7)        TEXT    "".f(SB), ABIInternal, $32-8 
        ...
        0x001d 00029 (main.go:7)        MOVQ    $0, "".result+40(SP)        ;; 將常量0 寫入40(SP)  
        0x0026 00038 (main.go:8)        MOVL    $8, (SP)                    ;; 將常量8 放入棧頂
        0x002d 00045 (main.go:8)        LEAQ    "".f.func1·f(SB), AX        ;; 將函數f.func1·f地址寫入AX
        0x0034 00052 (main.go:8)        MOVQ    AX, 8(SP)                   ;; 將函數f.func1·f地址寫入8(SP)
        0x0039 00057 (main.go:8)        LEAQ    "".result+40(SP), AX        ;; 將40(SP)地址值寫入AX
        0x003e 00062 (main.go:8)        MOVQ    AX, 16(SP)                  ;; 將AX 保存的地址寫入16(SP)
        0x0043 00067 (main.go:8)        PCDATA  $1, $0
        0x0043 00067 (main.go:8)        CALL    runtime.deferproc(SB)       ;; 調用 runtime.deferproc 函數

由於defer堆上分配會調用 runtime.deferproc函數,所以在這段彙編中展示的是 runtime.deferproc函數調用前的一段彙編,如果看過《從棧上理解 Go語言函數調用 //www.luozhiyun.com/archives/518 》,那麼上面這段是理解起來很簡單的。

因為 runtime.deferproc函數的參數就是兩個參數,如下:

func deferproc(siz int32, fn *funcval)

在函數調用過程中,參數的傳遞是從參數列表的右至左壓棧,所以在棧頂壓入的是常量8,在 8(SP) 位置壓入的是第二參數 f.func1·f函數地址。

看到這裡可能會有個疑問,在壓入常量8的時候大小是 int32 占 4 位元組大小,為啥第二參數不從 4(SP) 開始,而是要從 8(SP) 開始,這是因為需要做內存對齊導致的。

除了參數,還需要注意的是 16(SP) 位置壓入的是 40(SP) 的地址值。所以整個調用前的棧結構應該如下圖:

defer_call

下面我們看一下runtime.deferproc:

文件位置:src/runtime/panic.go

func deferproc(siz int32, fn *funcval) {  
	if getg().m.curg != getg() { 
		throw("defer on system stack")
	}
	// 獲取sp指針
	sp := getcallersp()
	// 獲取fn函數後指針作為參數
	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
	callerpc := getcallerpc()
	// 獲取一個新的defer
	d := newdefer(siz)
	if d._panic != nil {
		throw("deferproc: d.panic != nil after newdefer")
	}
    // 將 defer 加入到鏈表中
	d.link = gp._defer
	gp._defer = d
	d.fn = fn
	d.pc = callerpc
	d.sp = sp
    // 進行參數拷貝
	switch siz {
	case 0: 
        //如果defered函數的參數只有指針大小則直接通過賦值來拷貝參數
	case sys.PtrSize:
		// 將 argp 所對應的值 寫入到 deferArgs 返回的地址中
		*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
	default:
        // 如果參數大小不是指針大小,那麼進行數據拷貝
		memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
	}
 
	return0() 
}

在調用deferproc函數的時候,我們知道,參數siz傳入的是棧頂的值代表參數大小是 8 ,參數fn傳入的 8(SP)所對應的地址。

argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
...
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))

所以上面這兩句的配合實際上是將我們在上圖的 16(SP) 中保存的地址值存入到了 defer緊挨着的下一個8bytes的內存塊中作為 defer 的參數。簡單的畫一個示意圖應該是下面這樣,defer緊挨着的 argp裏面實際上存的是16(SP) 中保存的地址值:

defer_call2

需要注意的是,這裡會通過拷貝的操作來拷貝 argp 值,所以在 defer 被調用的時候,參數已經確定了,而不是等執行的時候才確定,不過這裡拷貝的一個地址值。

並且我們知道,在堆上分配時,defer會以鏈表的形式存放在當前的 Goroutine 中,如果有 3個 defer分別被調用,那麼最後調用的會在鏈表最前面:

deferLink

對於 newdefer這個函數來總的來說就是從 P 的本地緩存池裡獲取,獲取不到則從 sched 的全局緩存池裡獲取一半 defer 來填充 P 的本地資源池, 如果還是沒有可用的緩存,直接從堆上分配新的 deferargs 。這裡的內存分配和內存分配器分配的思路大致雷同,不再分析,感興趣的可以自己看一下。

下面我們繼續回到 f 函數的彙編中:

"".f STEXT size=126 args=0x8 locals=0x20 
        ...
        0x004e 00078 (main.go:11)       MOVQ    $6, "".result+40(SP)        ;; 將常量6寫入40(SP),作為返回值
        0x0057 00087 (main.go:11)       XCHGL   AX, AX
        0x0058 00088 (main.go:11)       CALL    runtime.deferreturn(SB)     ;; 調用 runtime.deferreturn 函數
        0x005d 00093 (main.go:11)       MOVQ    24(SP), BP
        0x0062 00098 (main.go:11)       ADDQ    $32, SP
        0x0066 00102 (main.go:11)       RET

這裡非常簡單,直接將常量6寫入到 40(SP) 中作為返回值,然後調用 runtime.deferreturn執行 defer

下面我們看一下runtime.deferreturn:

文件位置:src/runtime/panic.go

func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	// 確定 defer 的調用方是不是當前 deferreturn 的調用方
	sp := getcallersp()
	if d.sp != sp {
		return
	}
	 
	switch d.siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		// 將 defer 保存的參數複製出來
		// arg0 實際上是 caller SP 棧頂地址值,所以這裡實際上是將參數複製到 caller SP 棧頂地址值
		*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
	default:
        // 如果參數大小不是 sys.PtrSize,那麼進行數據拷貝
		memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
	}
	fn := d.fn
	d.fn = nil
	gp._defer = d.link
	//將 defer 對象放入到 defer 池中,後面可以復用
	freedefer(d)
	 
	_ = fn.fn
	// 傳入需要執行的函數和參數
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

首先要注意的是,這裡傳入的參數 arg0實際上是 caller 調用方的棧頂的值,所以下面這個賦值實際上是將 defer 的參數複製到 caller 調用方棧頂:

*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))

*(*uintptr)(deferArgs(d))這裡保存的實際上是 caller 調用方 16(SP) 保存的地址值。那麼 caller 調用方的棧幀如下圖所示:

callerstackframe

下面進入到 runtime.jmpdefer中看一下如何實現:

位置:src/runtime/asm_amd64.s

TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
	MOVQ	fv+0(FP), DX	// fn 函數地址
	MOVQ	argp+8(FP), BX	// caller sp 調用方 SP
	LEAQ	-8(BX), SP	//  caller 後的調用方 SP
	MOVQ	-8(SP), BP	//  caller 後的調用方 BP
	SUBQ	$5, (SP)	//  獲取 runtime.deferreturn 地址值寫入棧頂
	MOVQ	0(DX), BX   // BX = DX
	JMP	BX	// 執行被 defer 的函數

這段彙編非常有意思,jmpdefer函數由於是 runtime.deferreturn調用的,所以現在的調用棧幀是:

callingchain

傳入到 jmpdefer函數的參數是 0(FP) 表示 fn 函數地址,以及 8(FP) 表示的是 f 函數的調用棧的 SP。

callingchain2

所以下面這句代表的是 runtime.deferreturn調用棧的 return address寫入到 SP:

LEAQ	-8(BX), SP

那麼 -8(SP) 代表的是 runtime.deferreturn調用棧的 Base Pointer

MOVQ	-8(SP), BP

下面我們重點解釋一下為什麼要將 SP 指針指向的值減5可以獲取到 runtime.deferreturn的地址值:

SUBQ	$5, (SP)

我們回到 f 函數調用的彙編中:

(dlv) disass
TEXT main.f(SB) /data/gotest/main.go
		...
        main.go:11      0x45def8        e8a3e2fcff              call $runtime.deferreturn
        main.go:11      0x45defd        488b6c2418              mov rbp, qword ptr [rsp+0x18]
        ...

由於調用完 runtime.deferreturn 函數後需要繼續返回到 0x45defd 地址值處繼續執行,所以在調用 runtime.deferreturn 函數時對應的棧幀中 return address其實就是 0x45defd。

而在 jmpdefer函數中,(SP) 對應的值就是 runtime.deferreturn調用棧的 return address,所以將 0x45defd 減去5 剛好就可以獲取到 0x45def8 ,而這個值就是 runtime.deferreturn函數的地址值。

那麼在最後跳轉到 f.func1 函數執行的時候,調用棧如下:

callingchain3

調用棧 (SP) 的位置實際上存放的是指向 deferreturn 函數的指針,所以在f.func1函數調用完畢之後會再回到 deferreturn 函數,直到 _defer鏈中沒有數據為止:

func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	...
}

下面再簡短的看一下f.func1函數調用:

"".f.func1 STEXT nosplit size=25 args=0x8 locals=0x0
        0x0000 00000 (main.go:8)        TEXT    "".f.func1(SB), NOSPLIT|ABIInternal, $0-8
        0x0000 00000 (main.go:8)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
        0x0000 00000 (main.go:8)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0000 00000 (main.go:9)        MOVQ    "".&result+8(SP), AX        ;; 將指向6的地址值寫入 AX
        0x0005 00005 (main.go:9)        MOVQ    (AX), AX                    ;; 將 6 寫入到 AX
        0x0008 00008 (main.go:9)        LEAQ    (AX)(AX*2), CX              ;; CX = 6*2 +6 =18
        0x000c 00012 (main.go:9)        LEAQ    (AX)(CX*2), AX              ;; AX = 18*2 + 6 =42
        0x0010 00016 (main.go:9)        MOVQ    "".&result+8(SP), CX        ;; 將指向6的地址值寫入 CX
        0x0015 00021 (main.go:9)        MOVQ    AX, (CX)                    ;; 將CX地址值指向的值改為42
        0x0018 00024 (main.go:10)       RET

這裡調用就非常的簡單了,獲取到 8(SP) 地址值指向的數據然後做運算,然後再將結果寫入棧中,返回。

到這裡我們基本上把 defer的函數的調用整個過程通過堆上分配展示給大家看了。從上面的分析中基本也回答了defer 可以修改有名返回值函數的返回值是如何做到的,答案其實就是在defer調用過程中傳遞的 defer 參數是一個返回值的指針,所以最後在 defer執行的時候會修改返回值。

匿名函數返回值調用

那麼如果匿名返回值函數是如何傳遞的呢?例如下面這種:

// f returns 100
func f() int {
	i := 100
	defer func() {
		i++
	}()
	return i
}

下面打印一下彙編:

"".f STEXT size=139 args=0x8 locals=0x28
        0x0000 00000 (main.go:7)        TEXT    "".f(SB), ABIInternal, $40-8
        ...
        0x001d 00029 (main.go:7)        MOVQ    $0, "".~r0+48(SP)       ;;初始化返回值
        0x0026 00038 (main.go:8)        MOVQ    $100, "".i+24(SP)       ;;初始化參數i
        0x002f 00047 (main.go:9)        MOVL    $8, (SP)
        0x0036 00054 (main.go:9)        LEAQ    "".f.func1·f(SB), AX
        0x003d 00061 (main.go:9)        MOVQ    AX, 8(SP)               ;; 將f.func1·f地址值寫入8(SP)
        0x0042 00066 (main.go:9)        LEAQ    "".i+24(SP), AX
        0x0047 00071 (main.go:9)        MOVQ    AX, 16(SP)              ;; 將 24(SP) 地址值寫入到 16(SP) 
        0x004c 00076 (main.go:9)        PCDATA  $1, $0
        0x004c 00076 (main.go:9)        CALL    runtime.deferproc(SB)
        0x0051 00081 (main.go:9)        TESTL   AX, AX
        0x0053 00083 (main.go:9)        JNE     113
        0x0055 00085 (main.go:9)        JMP     87
        0x0057 00087 (main.go:12)       MOVQ    "".i+24(SP), AX         ;; 將24(SP)的值100寫入到AX
        0x005c 00092 (main.go:12)       MOVQ    AX, "".~r0+48(SP)       ;; 將值100寫入到48(SP)
        0x0061 00097 (main.go:12)       XCHGL   AX, AX
        0x0062 00098 (main.go:12)       CALL    runtime.deferreturn(SB)
        0x0067 00103 (main.go:12)       MOVQ    32(SP), BP
        0x006c 00108 (main.go:12)       ADDQ    $40, SP
        0x0070 00112 (main.go:12)       RET

在上面的輸出中我們可以看得出匿名返回值函數的調用中首先會將常量 100 寫入到 24(SP) 中,然後將 24(SP) 的地址值寫入到 16(SP) ,然後在寫返回值的時候是通過 MOVQ 指令將 24(SP) 的值寫入到 48(SP) 中,也就是說這裡完全是值複製,並沒有複製指針,所以也就沒有修改返回值。

小結

下面通過一個圖對比一下兩者的在調用完 runtime.deferreturn棧幀的情況:

stackframecompare

很明顯可以看出有名返回值函數會在 16(SP) 的地方保存返回值的地址,而匿名返回值函數會在 16(SP) 的地方保存24(SP) 的地址。

通過上面的一連串的分析也順帶回答了幾個問題:

  1. defer 是如何傳遞參數的?

    我們在上面分析的時候會發現,在執行 deferproc 函數的時候會先將參數值進行複製到 defer內存地址值緊挨着的位置作為參數,如果是指針的傳遞會直接複製指針,值傳遞會直接複製值到defer參數的位置。

    deferargs

    然後在執行deferreturn函數的時候會複製參數值到棧中,然後調用jmpdefer進行執行 。

    func deferreturn(arg0 uintptr) {
    	...
    	switch d.siz {
    	case 0:
    		// Do nothing.
    	case sys.PtrSize:
    		// 將 defer 保存的參數複製出來
    		// arg0 實際上是 caller SP 棧頂地址值,所以這裡實際上是將參數複製到 caller SP 棧頂地址值
    		*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
    	default:
            // 如果參數大小不是 sys.PtrSize,那麼進行數據拷貝
    		memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
    	}
    	...
    }
    
  2. 多個 defer 語句是如何執行?

    在調用 deferproc函數註冊 defer的時候會將新元素插在表頭,執行的時候也是獲取鏈表頭依次執行。

    deferLink

  3. defer、return、返回值的執行順序是怎樣的?

    對於這個問題,我們將上面例子中的輸出的彙編拿過來研究一下就明白了:

    "".f STEXT size=126 args=0x8 locals=0x20 
            ...
            0x004e 00078 (main.go:11)       MOVQ    $6, "".result+40(SP)        ;; 將常量6寫入40(SP),作為返回值
            0x0057 00087 (main.go:11)       XCHGL   AX, AX
            0x0058 00088 (main.go:11)       CALL    runtime.deferreturn(SB)     ;; 調用 runtime.deferreturn 函數
            0x005d 00093 (main.go:11)       MOVQ    24(SP), BP
            0x0062 00098 (main.go:11)       ADDQ    $32, SP
            0x0066 00102 (main.go:11)       RET
    

    從這段彙編中可以知道,對於

    1. 首先是最先設置返回值為常量6;
    2. 然後會調用 runtime.deferreturn執行 defer鏈表;
    3. 執行 RET 指令跳轉到 caller 函數;

棧上分配

在開始的時候也講到了,在 Go 的 1.13 版本之後加入了 defer 的棧上分配,所以和堆上分配有一個區別是在棧上創建 defer 的時候是通過 deferprocStack進行創建的。

Go 在編譯的時候在 SSA 階段會經過判斷,如果是棧上分配,那麼會需要直接在函數調用幀上使用編譯器來初始化 _defer 記錄,並作為參數傳遞給 deferprocStack。其他的執行過程和堆上分配並沒有什麼區別。

對於deferprocStack函數我們簡單看一下:

文件位置:src/cmd/compile/internal/gc/ssa.go

func deferprocStack(d *_defer) {
	gp := getg()
	if gp.m.curg != gp { 
		throw("defer on system stack")
	} 
	d.started = false
	d.heap = false  // 棧上分配的 _defer
	d.openDefer = false
	d.sp = getcallersp()
	d.pc = getcallerpc()
	d.framepc = 0
	d.varp = 0 
	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
	*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
	// 將多個 _defer 記錄通過鏈表進行串聯
	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

	return0() 
}

函數主要功能就是給 _defer結構體賦值,並返回。

開放編碼

Go 語言在 1.14 中通過代碼內聯優化,使得函數末尾直接對defer函數進行調用, 做到幾乎不需要額外的開銷。在 SSA 的構建階段 buildssa會根據檢查是否滿足條件,滿足條件才會插入開放編碼式,由於 SSA 的構建階段的代碼不太好理解,所以下面只給出基本原理,不涉及代碼分析。

我們可以對堆上分配的例子進行彙編打印:

$ GOOS=linux GOARCH=amd64 go tool compile -S  main.go
"".f STEXT size=155 args=0x8 locals=0x30
        0x0000 00000 (main.go:7)        TEXT    "".f(SB), ABIInternal, $48-8
        ...
        0x002e 00046 (main.go:7)        MOVQ    $0, "".~r0+56(SP)
        0x0037 00055 (main.go:8)        MOVQ    $100, "".i+16(SP)
        0x0040 00064 (main.go:9)        LEAQ    "".f.func1·f(SB), AX
        0x0047 00071 (main.go:9)        MOVQ    AX, ""..autotmp_4+32(SP)
        0x004c 00076 (main.go:9)        LEAQ    "".i+16(SP), AX
        0x0051 00081 (main.go:9)        MOVQ    AX, ""..autotmp_5+24(SP)
        0x0056 00086 (main.go:9)        MOVB    $1, ""..autotmp_3+15(SP)
        0x005b 00091 (main.go:12)       MOVQ    "".i+16(SP), AX
        0x0060 00096 (main.go:12)       MOVQ    AX, "".~r0+56(SP)
        0x0065 00101 (main.go:12)       MOVB    $0, ""..autotmp_3+15(SP)
        0x006a 00106 (main.go:12)       MOVQ    ""..autotmp_5+24(SP), AX
        0x006f 00111 (main.go:12)       MOVQ    AX, (SP)
        0x0073 00115 (main.go:12)       PCDATA  $1, $1
        0x0073 00115 (main.go:12)       CALL    "".f.func1(SB)	;; 直接調用 defer 函數
        0x0078 00120 (main.go:12)       MOVQ    40(SP), BP
        0x007d 00125 (main.go:12)       ADDQ    $48, SP
        0x0081 00129 (main.go:12)       RET

我們可以看到上面的的彙編輸出中直接將 defer 函數插入到函數末尾進行調用。

在上面的這個例子是很容易優化的,但是如果一個 defer 在一個條件語句中,這個條件必須要到運行時才能確定,那麼這又該如何優化呢?

在開放編碼中還使用了 defer bit 延遲比特來判斷條件分支是否該執行。這個 延遲比特長度是一個 8 位的二進制碼,所以在這項優化中最多只能使用 8 個defer,包括條件判斷裏面的defer。每一位是否被設置為 1,來判斷延遲語句是否在運行時被設置,如果設置,則發生調用。 否則則不調用。

比如說在下面這篇文章裏面講解了一個例子:

//go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md

defer f1(a)
if cond {
 defer f2(b)
}
body...

在創建延遲調用的階段,首先通過延遲比特的特定位置記錄哪些帶條件的 defer 被觸發。

deferBits := 0           // 初始值 00000000
deferBits |= 1 << 0     // 遇到第一個 defer,設置為 00000001
_f1 = f1
_a1 = a1
if cond {
	// 如果第二個 defer 被設置,則設置為 00000011,否則依然為 00000001
	deferBits |= 1 << 1
	_f2 = f2
	_a2 = a2
}

在函數返回退出前, exit 函數會依次倒序創建對延遲比特的檢查代碼:

exit:
// 判斷 deferBits & 00000010 == 00000010是否成立
if deferBits & 1<<1 != 0 {
 deferBits &^= 1<<1
 tmpF2(tmpB)
}
// 判斷 deferBits & 00000001  == 00000001 是否成立
if deferBits & 1<<0 != 0 {
 deferBits &^= 1<<0
 tmpF1(tmpA)
}

在函數退出前會判斷延遲比特與相應位置的數值進行取與來判斷該位置是否為 1,如果為 1,那麼表示可以執行該 defer 函數。

總結

本文主要講解了 defer的執行規則,以及對 defer 類型做了介紹。主要通過堆上分配來講解 defer 函數調用是如何做的,如:函數調用來理解” defer 的參數傳遞”、”多個 defer 語句是如何執行”、”以及 defer、return、返回值的執行順序是怎樣”的等這幾個問題。通過這樣的分析希望大家對 defer 能有更深入的了解。

Reference

延遲語句 //golang.design/under-the-hood/zh-cn/part1basic/ch03lang/defer/

defer //draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/#53-defer

Defer statements //golang.org/ref/spec#Defer_statements

Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case //go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md

Defer in Practice //exlskills.com/learn-en/courses/aap-learn-go-golang–learn_golang_asap/aap-learn–asapgo/beyond-the-basics-YvUaZMowIAIo/defer-aqcApCogZxuV

脫胎換骨的defer //mp.weixin.qq.com/s/gaC2gmFhJezH-9-uxpz07w

Go defer 深度剖析篇(3)—— 源碼分析,深度原理剖析 //zhuanlan.zhihu.com/p/351177696

Inlined defers in Go //rakyll.org/inlined-defers/

Go 程序是如何編譯成目標機器碼的 //segmentfault.com/a/1190000016523685

luozhiyun很酷

Tags: