轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格: https://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語言函式呼叫 https://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 原始碼 https://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語言函式呼叫 https://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,來判斷延遲語句是否在執行時被設定,如果設定,則發生呼叫。 否則則不呼叫。
比如說在下面這篇文章裡面講解了一個例子:
https://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
延遲語句 https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/defer/
defer https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/#53-defer
Defer statements https://golang.org/ref/spec#Defer_statements
Proposal: Low-cost defers through inline code, and extra funcdata to manage the panic case https://go.googlesource.com/proposal/+/refs/heads/master/design/34481-opencoded-defers.md
Defer in Practice https://exlskills.com/learn-en/courses/aap-learn-go-golang--learn_golang_asap/aap-learn--asapgo/beyond-the-basics-YvUaZMowIAIo/defer-aqcApCogZxuV
脫胎換骨的defer https://mp.weixin.qq.com/s/gaC2gmFhJezH-9-uxpz07w
Go defer 深度剖析篇(3)—— 原始碼分析,深度原理剖析 https://zhuanlan.zhihu.com/p/351177696
Inlined defers in Go https://rakyll.org/inlined-defers/
Go 程式是如何編譯成目標機器碼的 https://segmentfault.com/a/1190000016523685