Skip to content

defer

defer è una parola chiave che appare molto frequentemente nello sviluppo quotidiano in Go. Esegue le funzioni associate a defer in ordine LIFO (Last In First Out). Spesso usiamo questo meccanismo per operazioni di rilascio risorse, come la chiusura di file.

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

L'alta frequenza di apparizione di questa parola chiave rende necessario comprendere la struttura sottostante.

Struttura

La parola chiave defer corrisponde alla struttura runtime._defer. La sua struttura non è complessa:

go
type _defer struct {
  started bool
  heap    bool
  openDefer bool
  sp        uintptr // sp al momento del defer
  pc        uintptr // pc al momento del defer
  fn        func()  // può essere nil per defer open-coded
  _panic    *_panic // panic che sta eseguendo defer
  link      *_defer // prossimo defer su G; può puntare a heap o stack!
  fd   unsafe.Pointer // funcdata per la funzione associata al frame
  varp uintptr        // valore di varp per lo stack frame
  framepc uintptr
}

Il campo fn è la funzione corrispondente alla parola chiave defer, link indica il prossimo defer collegato, sp e pc registrano le informazioni sulla funzione chiamante, usate per determinare a quale funzione appartiene il defer. defer esiste come lista concatenata a runtime, la testa della lista si trova sulla goroutine G, quindi defer è effettivamente associato direttamente alla goroutine.

go
type g struct {
    ...
  _panic    *_panic // panic più interno - offset noto a liblink
  _defer    *_defer // defer più interno
    ...
}

Quando la goroutine esegue una funzione, i defer nella funzione vengono aggiunti dalla testa della lista in ordine.

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

Quel codice sopra corrisponde a questa figura:

Oltre alla goroutine, anche P ha una certa relazione con defer. Nella struttura di P, c'è un campo deferpool, come mostrato:

go
type p struct {
  ...
  deferpool    []*_defer // pool di strutture defer disponibili (vedi panic.go)
  deferpoolbuf [32]*_defer
    ...
}

deferpool contiene strutture defer preallocate, usate per allocare nuove strutture defer per la goroutine G associata a P, riducendo l'overhead.

Allocazione

A livello sintattico, l'uso della parola chiave defer viene tradotto dal compilatore in una chiamata alla funzione runtime.deferproc. Ad esempio, il codice Go è scritto così:

go
defer fn1(x, y)

Ma dopo la compilazione, il codice effettivo è così:

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

Quindi in realtà la funzione passata a defer non ha parametri né valori di ritorno. La funzione deferproc è la seguente:

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

Questa funzione è responsabile della creazione della struttura defer e del suo inserimento nella testa della lista della goroutine G. La funzione runtime.newdefer tenta di ottenere la struttura defer preallocata da deferpool in 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)
}

Per prima cosa riempie metà delle strutture defer da sched.deferpool globale a deferpool locale, poi tenta di ottenere da deferpool in 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 {
    // Alloca nuovo defer.
    d = new(_defer)
}
d.heap = true

Solo se non si trova nulla si usa l'allocazione manuale. Infine si può vedere questo codice:

go
d.heap = true

Questo indica che defer è allocato sull'heap. Quando è false, viene allocato sullo stack. La memoria allocata sullo stack viene automaticamente recuperata al ritorno, e la sua efficienza di gestione della memoria è maggiore rispetto all'heap. Il fattore che decide se allocare sullo stack è il numero di livelli di ciclo. Questa logica può essere rintracciata nel metodo escape.goDeferStmt in 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 indica il numero di livelli di ciclo dell'istruzione corrente. Se l'istruzione defer non è in un ciclo, verrà allocata sullo 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)
    }

Se è allocata sullo stack, la struttura defer viene creata direttamente sullo stack, e infine la funzione runtime.deferprocStack completa la creazione della struttura defer.

go
if k == callDeferStack {
    // Crea una struttura defer d sullo stack.
    if stksize != 0 {
      s.Fatalf("deferprocStack con dimensione stack non-zero %d: %v", stksize, n)
    }

    t := deferstruct()
    ...
    // Chiama runtime.deferprocStack con puntatore al 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 prende un argomento *_defer

La firma della funzione deferprocStack è:

go
func deferprocStack(d *_defer)

La logica specifica di creazione non è molto diversa da deferproc. La differenza principale è che nell'allocazione sullo stack la struttura defer proviene da una struttura creata direttamente, mentre nell'allocazione sull'heap proviene dalla funzione new.

Esecuzione

Quando la funzione sta per tornare o si verifica un panic, si entra nella funzione runtime.deferreturn, responsabile di prelevare defer dalla lista della goroutine ed eseguirlo.

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

Per prima cosa ottiene lo stack frame della funzione corrente tramite getcallersp() e lo confronta con sp nella struttura defer per determinare se defer appartiene alla funzione corrente. Poi preleva la struttura defer dalla testa della lista, esegue il prossimo defer con gp._defer = d.link, rilascia la struttura defer nel pool tramite la funzione runtuime.freedefer, infine chiama fn per eseguire. Continua così finché non ha eseguito tutti i defer appartenenti alla funzione corrente.

Open-Coded

L'uso di defer non è senza costi. Sebbene fornisca comodità a livello sintattico, non è una chiamata diretta di funzione e deve passare attraverso una serie di processi, causando comunque perdite di prestazioni. Quindi successivamente gli sviluppatori Go hanno progettato un'ottimizzazione: open-coded. È un modo per ottimizzare defer. Il nome originale inglese è open-coded, che in Cina viene tradotto come "codifica aperta", ma qui "open" si riferisce all'espansione, ovvero espandere il codice della funzione defer nel codice della funzione corrente, come l'inlining delle funzioni. Questo metodo di ottimizzazione ha le seguenti limitazioni:

  1. Il numero di defer nella funzione non può superare 8
  2. Il prodotto del numero di defer e return non può superare 15
  3. defer non può apparire in un ciclo
  4. L'ottimizzazione della compilazione non deve essere disabilitata
  5. Non ci sono chiamate manuali a os.Exit()
  6. Non è necessario copiare parametri dall'heap

Questa logica di判断 può essere rintracciata nella funzione 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
}

Poi Go crea una variabile intera a 8 bit deferBits nella funzione corrente come bitmap per marcare defer. Ogni bit marca uno, un intero a 8 bit uint8 può rappresentare al massimo 8. Se il bit corrispondente è 1, il defer ottimizzato con open-coded verrà eseguito quando la funzione deve tornare.

Golang by www.golangdev.cn edit