defer
defer ใน go เป็นคำหลักที่ปรากฏบ่อยมากในการพัฒนาประจำวัน จะดำเนินการฟังก์ชันที่เกี่ยวข้องกับ defer ตามลำดับแบบเข้าก่อนออกหลัง มักใช้กลไกนี้สำหรับการปล่อยทรัพยากร เช่น การปิดไฟล์
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...คำหลักที่ปรากฏบ่อยเช่นนี้ ทำให้เราจำเป็นต้องเข้าใจโครงสร้างเบื้องหลังของมัน
โครงสร้าง
คำหลัก defer สอดคล้องกับโครงสร้าง runtime._defer โครงสร้างของมันไม่ซับซ้อนมาก
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer // next defer on G; can point to either heap or stack!
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
framepc uintptr
}ฟิลด์ fn เป็นฟังก์ชันที่ตรงกับคำหลัก defer link แสดงถึง defer ถัดไปที่เชื่อมโยง sp และ pc บันทึกข้อมูลฟังก์ชันของผู้เรียก ใช้สำหรับตัดสินว่า defer เป็นของฟังก์ชันใด defer ในขณะทำงานมีรูปแบบเป็นลิงก์รายการ หัวของลิงก์อยู่บน G ดังนั้น defer จึงเชื่อมโยงโดยตรงกับ G
type g struct {
...
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
...
}เมื่อ G ดำเนินการฟังก์ชัน จะเพิ่ม defer ในฟังก์ชันตามลำดับจากหัวของลิงก์
defer fn1()
defer fn2()
defer fn3()โค้ดนั้นตรงกับภาพนี้

นอกจาก G แล้ว P ก็มีความเกี่ยวข้องกับ defer ในโครงสร้างของ P มีฟิลด์ deferpool ตามที่แสดง
type p struct {
...
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
...
}ใน deferpool เก็บโครงสร้าง defer ที่จัดเตรียมไว้ล่วงหน้า ใช้สำหรับจัดสรรโครงสร้าง defer ใหม่ให้กับ G ที่เชื่อมโยงกับ P สามารถลดค่าใช้จ่ายได้
การจัดสรร
ในทางไวยากรณ์สำหรับการใช้คำหลัก defer คอมไพเลอร์จะแปลงเป็นการเรียกฟังก์ชัน runtime.deferproc เช่น โค้ด go เขียนแบบนี้
defer fn1(x, y)แต่หลังจากคอมไพล์แล้วโค้ดจริงเป็นแบบนี้
deferproc(func(){
fn1(x, y)
})ดังนั้นจริงๆ แล้ว defer ส่งเข้าฟังก์ชันไม่มีพารามิเตอร์และไม่มีค่าส่งคืน ฟังก์ชัน deferproc มีโค้ดดังนี้
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}ฟังก์ชันนี้รับผิดชอบสร้างโครงสร้าง defer และเพิ่มเข้าไปในหัวของลิงก์ G ในนั้นฟังก์ชัน runtime.newdefer จะลองรับโครงสร้าง defer ที่จัดเตรียมไว้ล่วงหน้าจาก deferpool ใน P
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}อย่างแรกจะเติมโครงสร้าง defer จาก sched.deferpool ระดับโลกไปยัง deferpool ระดับท้องถิ่นครึ่งหนึ่ง แล้วลองรับจาก deferpool ใน P
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Allocate new defer.
d = new(_defer)
}
d.heap = trueสุดท้ายหากไม่พบจริงๆ จึงจะใช้วิธีการจัดสรรด้วยตนเอง สุดท้ายจะเห็นว่ามีโค้ดแบบนี้
d.heap = trueนี่แสดงว่า defer จัดสรรบน heap เมื่อเป็น false จะจัดสรรบน stack การจัดการ memory ที่จัดสรรบน stack จะถูก回收อัตโนมัติเมื่อกลับ ประสิทธิภาพการจัดการ memory สูงกว่าบน heap ปัจจัยที่ตัดสินว่าจะจัดสรรบน stack หรือไม่คือจำนวน layer ของ loop ส่วนตรรกะนี้สามารถติดตามไปยังวิธีการ escape.goDeferStmt ใน cmd/compile/ssagen ดังนี้
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}e.loopDepth แสดงถึงจำนวน layer ของ loop ของคำสั่งปัจจุบัน หากคำสั่ง defer ปัจจุบันไม่อยู่ใน loop จะจัดสรรบน stack
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}หากจัดสรรบน stack จะสร้างโครงสร้าง defer บน stack โดยตรง สุดท้ายจะเสร็จโดยฟังก์ชัน runtime.deferprocStack
if k == callDeferStack {
// Make a defer struct d on the stack.
if stksize != 0 {
s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
}
t := deferstruct()
...
// Call runtime.deferprocStack with pointer to _defer record.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer argลายเซ็นฟังก์ชัน deferprocStack เป็นดังนี้
func deferprocStack(d *_defer)ตรรกะการสร้างเฉพาะของฟังก์ชันไม่แตกต่างจาก deferproc มากนัก ความแตกต่างหลักคือ เมื่อจัดสรรบน stack แหล่งที่มาของโครงสร้าง defer คือโครงสร้างที่สร้างโดยตรง แหล่งที่มาของ defer ที่จัดสรรบน heap คือฟังก์ชัน new
การดำเนินการ
เมื่อฟังก์ชันกำลังจะกลับหรือเกิด panic จะเข้าสู่ฟังก์ชัน runtime.deferreturn รับผิดชอบรับ defer จากลิงก์ของ G และดำเนินการ
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}อย่างแรกจะใช้ getcallersp() รับ stack frame ปัจจุบันและเปรียบเทียบกับ sp ในโครงสร้าง defer เพื่อตัดสินว่า defer เป็นของฟังก์ชันปัจจุบันหรือไม่ จากนั้นนำโครงสร้าง defer ออกจากหัวของลิงก์ และใช้ gp._defer = d.link ดำเนินการ defer ถัดไป แล้วใช้ฟังก์ชัน runtuime.freedefer ปล่อยโครงสร้าง defer กลับสู่ pool สุดท้ายเรียก fn ดำเนินการ ทำเช่นนี้ต่อไปจนกว่าจะดำเนินการ defer ทั้งหมดที่เป็นของฟังก์ชันปัจจุบันเสร็จ
การเข้ารหัสแบบเปิด
การใช้ defer ไม่ได้ไม่มีต้นทุน แม้จะให้ความสะดวกในทางไวยากรณ์ แต่毕竟ไม่ใช่การเรียกฟังก์ชันโดยตรง ต้องผ่านกระบวนการหลายอย่าง จึงทำให้เกิดการสูญเสียประสิทธิภาพ ดังนั้นต่อมา go ทางการจึงออกแบบวิธีการปรับปรุงประสิทธิภาพ——การเข้ารหัสแบบเปิด เป็นวิธีการปรับปรุงประสิทธิภาพสำหรับ defer ชื่อภาษาอังกฤษเดิมคือ open-coded ในประเทศส่วนใหญ่แปลเป็นการเข้ารหัสแบบเปิด open ที่นี่หมายถึงการ expand就是将โค้ดฟังก์ชัน defer expand เป็นโค้ดฟังก์ชันปัจจุบัน เหมือนกับการ inline ฟังก์ชัน วิธีการปรับปรุงประสิทธิภาพนี้มีข้อจำกัดดังนี้
- จำนวน
deferในฟังก์ชันต้องไม่เกิน 8 ตัว - ผลคูณของจำนวน
deferและreturnต้องไม่เกิน 15 deferต้องไม่ปรากฏใน loop- ไม่ปิดการปรับปรุงประสิทธิภาพการคอมไพล์
- ไม่มีการเรียก
os.Exit()ด้วยตนเอง - ไม่ต้องคัดลอกพารามิเตอร์จาก heap
ส่วนตรรกะการตัดสินนี้สามารถติดตามไปยังส่วนโค้ดด้านล่างของฟังก์ชัน cmd/compile/ssagen.buildssa
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}แล้ว go จะสร้างตัวแปรจำนวนเต็ม 8 บิต deferBits ในฟังก์ชันปัจจุบันเพื่อใช้เป็น bitmap สำหรับ标记 defer แต่ละบิต标记หนึ่งตัว จำนวนเต็ม uint8 8 บิตแสดงได้มากที่สุด 8 ตัว หากบิตที่ตรงกันเป็น 1 defer ที่ปรับปรุงประสิทธิภาพด้วยการเข้ารหัสแบบเปิดที่ตรงกันจะดำเนินการเมื่อฟังก์ชันจะกลับ
