Skip to content

defer

defer ใน go เป็นคำหลักที่ปรากฏบ่อยมากในการพัฒนาประจำวัน จะดำเนินการฟังก์ชันที่เกี่ยวข้องกับ defer ตามลำดับแบบเข้าก่อนออกหลัง มักใช้กลไกนี้สำหรับการปล่อยทรัพยากร เช่น การปิดไฟล์

go
fd, err := os.Open("/dev/stdin")
if err != nil{
    return err
}
defer fd.Close()
...

คำหลักที่ปรากฏบ่อยเช่นนี้ ทำให้เราจำเป็นต้องเข้าใจโครงสร้างเบื้องหลังของมัน

โครงสร้าง

คำหลัก defer สอดคล้องกับโครงสร้าง runtime._defer โครงสร้างของมันไม่ซับซ้อนมาก

go
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

go
type g struct {
    ...
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
    ...
}

เมื่อ G ดำเนินการฟังก์ชัน จะเพิ่ม defer ในฟังก์ชันตามลำดับจากหัวของลิงก์

go
defer fn1()
defer fn2()
defer fn3()

โค้ดนั้นตรงกับภาพนี้

นอกจาก G แล้ว P ก็มีความเกี่ยวข้องกับ defer ในโครงสร้างของ P มีฟิลด์ deferpool ตามที่แสดง

go
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 เขียนแบบนี้

go
defer fn1(x, y)

แต่หลังจากคอมไพล์แล้วโค้ดจริงเป็นแบบนี้

go
deferproc(func(){
  fn1(x, y)
})

ดังนั้นจริงๆ แล้ว defer ส่งเข้าฟังก์ชันไม่มีพารามิเตอร์และไม่มีค่าส่งคืน ฟังก์ชัน deferproc มีโค้ดดังนี้

go
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

go
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

go
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

สุดท้ายหากไม่พบจริงๆ จึงจะใช้วิธีการจัดสรรด้วยตนเอง สุดท้ายจะเห็นว่ามีโค้ดแบบนี้

go
d.heap = true

นี่แสดงว่า defer จัดสรรบน heap เมื่อเป็น false จะจัดสรรบน stack การจัดการ memory ที่จัดสรรบน stack จะถูก回收อัตโนมัติเมื่อกลับ ประสิทธิภาพการจัดการ memory สูงกว่าบน heap ปัจจัยที่ตัดสินว่าจะจัดสรรบน stack หรือไม่คือจำนวน layer ของ loop ส่วนตรรกะนี้สามารถติดตามไปยังวิธีการ escape.goDeferStmt ใน cmd/compile/ssagen ดังนี้

go
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

go
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

go
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 เป็นดังนี้

go
func deferprocStack(d *_defer)

ตรรกะการสร้างเฉพาะของฟังก์ชันไม่แตกต่างจาก deferproc มากนัก ความแตกต่างหลักคือ เมื่อจัดสรรบน stack แหล่งที่มาของโครงสร้าง defer คือโครงสร้างที่สร้างโดยตรง แหล่งที่มาของ defer ที่จัดสรรบน heap คือฟังก์ชัน new

การดำเนินการ

เมื่อฟังก์ชันกำลังจะกลับหรือเกิด panic จะเข้าสู่ฟังก์ชัน runtime.deferreturn รับผิดชอบรับ defer จากลิงก์ของ G และดำเนินการ

go
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 ฟังก์ชัน วิธีการปรับปรุงประสิทธิภาพนี้มีข้อจำกัดดังนี้

  1. จำนวน defer ในฟังก์ชันต้องไม่เกิน 8 ตัว
  2. ผลคูณของจำนวน defer และ return ต้องไม่เกิน 15
  3. defer ต้องไม่ปรากฏใน loop
  4. ไม่ปิดการปรับปรุงประสิทธิภาพการคอมไพล์
  5. ไม่มีการเรียก os.Exit() ด้วยตนเอง
  6. ไม่ต้องคัดลอกพารามิเตอร์จาก heap

ส่วนตรรกะการตัดสินนี้สามารถติดตามไปยังส่วนโค้ดด้านล่างของฟังก์ชัน cmd/compile/ssagen.buildssa

go
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 ที่ปรับปรุงประสิทธิภาพด้วยการเข้ารหัสแบบเปิดที่ตรงกันจะดำเนินการเมื่อฟังก์ชันจะกลับ

Golang by www.golangdev.cn edit