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.
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:
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.
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.
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:
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ì:
defer fn1(x, y)Ma dopo la compilazione, il codice effettivo è così:
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:
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.
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:
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 = trueSolo se non si trova nulla si usa l'allocazione manuale. Infine si può vedere questo codice:
d.heap = trueQuesto 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:
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.
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.
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 *_deferLa firma della funzione deferprocStack è:
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.
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:
- Il numero di
defernella funzione non può superare 8 - Il prodotto del numero di
deferereturnnon può superare 15 defernon può apparire in un ciclo- L'ottimizzazione della compilazione non deve essere disabilitata
- Non ci sono chiamate manuali a
os.Exit() - Non è necessario copiare parametri dall'heap
Questa logica di判断 può essere rintracciata nella funzione 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
}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.
