深入 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
都是在堆上分配,該機制在編譯時:
- 在
defer
語句的位置插入runtime.deferproc
,被執行時,defer
調用會保存為一個runtime._defer
結構體,存入 Goroutine 的_defer
鏈表的最前面; - 在函數返回之前的位置插入
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
調用直接插入函數返回之前,省去了運行時的 deferproc
或 deferprocStack
操作。,該優化可以將 defer
的調用開銷從 1.13 版本的 ~35ns 降低至 ~6ns 左右。
不過需要滿足一定的條件才能觸發:
- 沒有禁用編譯器優化,即沒有設置
-gcflags "-N"
; - 函數內
defer
的數量不超過 8 個,且return
語句與defer
語句個數的乘積不超過 15; - 函數的
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
}
上面需要注意的幾個參數是 siz
、heap
、fn
、link
、openDefer
這些參數會在下面的分析中講到。
分析
本文分析時,先從堆上分配講起,會順帶講一下 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) 的地址值。所以整個調用前的棧結構應該如下圖:
下面我們看一下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) 中保存的地址值:
需要注意的是,這裡會通過拷貝的操作來拷貝 argp 值,所以在 defer
被調用的時候,參數已經確定了,而不是等執行的時候才確定,不過這裡拷貝的一個地址值。
並且我們知道,在堆上分配時,defer
會以鏈表的形式存放在當前的 Goroutine 中,如果有 3個 defer
分別被調用,那麼最後調用的會在鏈表最前面:
對於 newdefer
這個函數來總的來說就是從 P 的本地緩存池裡獲取,獲取不到則從 sched 的全局緩存池裡獲取一半 defer
來填充 P 的本地資源池, 如果還是沒有可用的緩存,直接從堆上分配新的 defer
和 args
。這裡的內存分配和內存分配器分配的思路大致雷同,不再分析,感興趣的可以自己看一下。
下面我們繼續回到 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 調用方的棧幀如下圖所示:
下面進入到 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
調用的,所以現在的調用棧幀是:
傳入到 jmpdefer
函數的參數是 0(FP) 表示 fn 函數地址,以及 8(FP) 表示的是 f 函數的調用棧的 SP。
所以下面這句代表的是 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
函數執行的時候,調用棧如下:
調用棧 (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
棧幀的情況:
很明顯可以看出有名返回值函數會在 16(SP) 的地方保存返回值的地址,而匿名返回值函數會在 16(SP) 的地方保存24(SP) 的地址。
通過上面的一連串的分析也順帶回答了幾個問題:
-
defer 是如何傳遞參數的?
我們在上面分析的時候會發現,在執行
deferproc
函數的時候會先將參數值進行複製到defer
內存地址值緊挨着的位置作為參數,如果是指針的傳遞會直接複製指針,值傳遞會直接複製值到defer
參數的位置。然後在執行
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)) } ... }
-
多個 defer 語句是如何執行?
在調用
deferproc
函數註冊defer
的時候會將新元素插在表頭,執行的時候也是獲取鏈表頭依次執行。 -
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
從這段彙編中可以知道,對於
- 首先是最先設置返回值為常量6;
- 然後會調用
runtime.deferreturn
執行defer
鏈表; - 執行 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