Skip to content

defer

Kata kunci defer adalah kata kunci yang sangat sering muncul dalam pengembangan go sehari-hari, ia akan mengeksekusi fungsi yang terkait dengan defer dalam urutan LIFO (Last In First Out), seringkali kita memanfaatkan mekanisme ini untuk melakukan operasi pelepasan sumber daya, seperti operasi penutupan file.

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

Kata kunci yang sering muncul seperti ini membuat kita perlu memahami struktur di baliknya.

Struktur

Kata kunci defer sesuai dengan struktur runtime._defer, strukturnya tidak terlalu kompleks

go
type _defer struct {
  started bool
  heap    bool
  openDefer bool
  sp        uintptr // sp pada saat defer
  pc        uintptr // pc pada saat defer
  fn        func()  // bisa nil untuk defer open-coded
  _panic    *_panic // panic yang sedang menjalankan defer
  link      *_defer // defer berikutnya pada G; bisa menunjuk ke heap atau stack!
  fd   unsafe.Pointer // funcdata untuk fungsi yang terkait dengan frame
  varp uintptr        // nilai varp untuk stack frame
  framepc uintptr
}

Field fn di antaranya adalah fungsi yang sesuai dengan kata kunci defer, link menunjukkan defer berikutnya, sp dan pc mencatat informasi fungsi pemanggil, digunakan untuk menentukan defer milik fungsi mana. defer ada dalam bentuk linked list pada runtime, kepala linked list ada di goroutine G, jadi defer sebenarnya terkait langsung dengan goroutine.

go
type g struct {
    ...
  _panic    *_panic // panic terdalam - offset diketahui oleh liblink
  _defer    *_defer // defer terdalam
    ...
}

Ketika goroutine mengeksekusi fungsi, defer dalam fungsi akan ditambahkan ke kepala linked list secara berurutan

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

Kode di atas sesuai dengan gambar berikut

Selain goroutine, P juga memiliki hubungan tertentu dengan defer, dalam struktur P, ada field deferpool, seperti yang ditunjukkan.

go
type p struct {
  ...
  deferpool    []*_defer // pool struktur defer yang tersedia (lihat panic.go)
  deferpoolbuf [32]*_defer
    ...
}

Di deferpool存放的是预分配好的defer结构,用于给与 P 关联的 goroutine G 分配新的defer结构,可以减少开销。

Alokasi

Dalam sintaks penggunaan kata kunci defer, compiler akan mengubahnya menjadi pemanggilan fungsi runtime.deferproc. Misalnya kode go seperti ini

go
defer fn1(x, y)

Sedangkan setelah dikompilasi kode sebenarnya seperti ini

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

Jadi sebenarnya fungsi yang传入 defer tidak memiliki parameter dan tidak memiliki nilai return, kode fungsi deferproc adalah sebagai berikut

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()
}

Fungsi ini bertanggung jawab untuk membuat struktur defer dan menambahkannya ke kepala linked list goroutine G, fungsi runtime.newdefer di antaranya akan mencoba mendapatkan struktur defer yang sudah dialokasikan sebelumnya dari deferpool di 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)
}

Pertama-tama ia akan mengisi setengah defer dari sched.deferpool global ke deferpool lokal, kemudian mencoba mendapatkan dari deferpool di 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 {
    // Alokasikan defer baru.
    d = new(_defer)
}
d.heap = true

Terakhir jika benar-benar tidak ditemukan baru akan menggunakan cara alokasi manual. Terakhir dapat dilihat ada kode seperti ini

go
d.heap = true

Ini menunjukkan defer dialokasikan di heap, sesuai dengan ketika false, akan dialokasikan di stack, memori yang dialokasikan di stack akan otomatis dikembalikan saat return, efisiensi manajemen memorinya lebih tinggi daripada di heap, dan faktor yang menentukan apakah dialokasikan di stack adalah jumlah lapisan loop, bagian logika ini dapat ditelusuri ke metode escape.goDeferStmt di cmd/compile/ssagen seperti berikut

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 menunjukkan jumlah lapisan loop untuk pernyataan saat ini, jika pernyataan defer saat ini tidak berada dalam loop, akan dialokasikan di 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)
    }

Jika dialokasikan di stack, akan langsung membuat struktur defer di stack, akhirnya akan diselesaikan oleh fungsi runtime.deferprocStack untuk membuat struktur defer.

go
if k == callDeferStack {
    // Buat struktur defer d di stack.
    if stksize != 0 {
      s.Fatalf("deferprocStack dengan ukuran stack non-nol %d: %v", stksize, n)
    }

    t := deferstruct()
    ...
    // Panggil runtime.deferprocStack dengan pointer ke record *_defer.
    ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
    aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
    callArgs = append(callArgs, addr, s.mem())
    call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
    call.AddArgs(callArgs...)
    call.AuxInt = int64(types.PtrSize) // deferprocStack mengambil argumen *_defer

Signature fungsi deferprocStack adalah sebagai berikut

go
func deferprocStack(d *_defer)

Logika pembuatan spesifiknya tidak terlalu berbeda dengan deferproc, perbedaan utamanya adalah, saat alokasi di stack sumber struktur defer adalah struktur yang dibuat langsung, sumber defer yang dialokasikan di heap adalah fungsi new.

Eksekusi

Ketika fungsi akan return atau terjadi panic, akan masuk ke fungsi runtime.deferreturn, fungsi ini bertanggung jawab untuk mengambil defer dari linked list goroutine dan mengeksekusinya.

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()
  }
}

Pertama-tama akan mendapatkan stack frame fungsi saat ini melalui getcallersp() dan membandingkannya dengan sp dalam struktur defer untuk menentukan apakah defer milik fungsi saat ini, kemudian mengambil struktur defer dari kepala linked list, dan menggunakan gp._defer = d.link untuk mengeksekusi defer berikutnya, kemudian melepaskan struktur defer kembali ke pool melalui fungsi runtuime.freedefer, terakhir memanggil fn untuk mengeksekusi, terus berulang seperti ini sampai selesai mengeksekusi semua defer milik fungsi saat ini.

Open-coded

Penggunaan defer bukan tanpa biaya, meskipun dalam sintaks memberikan kemudahan bagi kita, tetapi bagaimanapun juga bukan pemanggilan fungsi langsung, di antaranya akan melalui serangkaian proses, jadi tetap akan menyebabkan kerugian performa, jadi kemudian go resmi merancang sebuah cara optimasi——open-coded, ini adalah cara optimasi untuk defer, nama Inggris aslinya adalah open-coded, di dalam negeri pada dasarnya diterjemahkan menjadi open-coded, open di sini berarti展开,yaitu展开 kode fungsi defer ke dalam kode fungsi saat ini, seperti fungsi inlining. Cara optimasi ini memiliki beberapa kondisi pembatas berikut

  1. Jumlah defer dalam fungsi tidak boleh melebihi 8
  2. Hasil kali jumlah defer dan return tidak boleh melebihi 15
  3. defer tidak boleh muncul dalam loop
  4. Optimasi kompilasi tidak dinonaktifkan
  5. Tidak secara manual memanggil os.Exit()
  6. Tidak perlu menyalin parameter dari heap

Bagian logika判断 ini dapat ditelusuri ke bagian kode berikut dari fungsi 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
}

Kemudian go akan membuat variabel integer 8-bit deferBits dalam fungsi saat ini untuk digunakan sebagai bitmap untuk menandai defer, setiap bit menandai satu, integer 8-bit uint8 paling banyak dapat表示 8, jika bit yang sesuai adalah 1, maka defer yang dioptimalkan dengan open-coded akan dieksekusi saat fungsi akan return.

Golang by www.golangdev.cn edit