defer
defer est un mot-clé très fréquent dans le développement quotidien en Go. Il exécute les fonctions associées à defer dans l'ordre inverse (dernier entré, premier sorti). Cette mécanique est souvent utilisée pour libérer des ressources, comme la fermeture de fichiers.
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...Ce mot-clé étant si fréquemment utilisé, il est nécessaire de comprendre sa structure sous-jacente.
Structure
Le mot-clé defer correspond à la structure runtime._defer, dont la structure n'est pas complexe :
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
}Le champ fn est la fonction associée au mot-clé defer, link indique le prochain defer dans la chaîne, sp et pc enregistrent les informations de la fonction appelante, utilisés pour déterminer à quelle fonction appartient le defer. À l'exécution, defer existe sous forme de liste chaînée, dont la tête se trouve sur la goroutine G, donc defer est directement associé à la goroutine.
type g struct {
...
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
...
}Lorsqu'une goroutine exécute une fonction, elle ajoute les defer de la fonction en tête de la liste chaînée, dans l'ordre :
defer fn1()
defer fn2()
defer fn3()Le code ci-dessus correspond à cette illustration :

En plus de la goroutine, P est également associé à defer. Dans la structure de P, il y a un champ deferpool, comme montré ci-dessous :
type p struct {
...
deferpool []*_defer // pool of available defer structs (see panic.go)
deferpoolbuf [32]*_defer
...
}deferpool contient des structures defer pré-allouées, utilisées pour allouer de nouvelles structures defer à la goroutine G associée à P, ce qui réduit les coûts.
Allocation
Au niveau syntaxique, l'utilisation du mot-clé defer est convertie par le compilateur en un appel à la fonction runtime.deferproc. Par exemple, si le code Go s'écrit :
defer fn1(x, y)Après compilation, le code réel est :
deferproc(func(){
fn1(x, y)
})Donc en réalité, la fonction passée à defer n'a ni paramètres ni valeur de retour. Le code de la fonction deferproc est le suivant :
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()
}Cette fonction est responsable de créer la structure defer et de l'ajouter en tête de la liste chaînée de la goroutine G. La fonction runtime.newdefer tente d'obtenir une structure defer pré-allouée depuis le deferpool de 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)
}Elle remplit d'abord la moitié du deferpool local depuis le sched.deferpool global, puis tente d'obtenir depuis le deferpool de 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 = trueEnfin, si aucune structure n'est trouvée, une allocation manuelle est effectuée. On peut voir ce code :
d.heap = trueCela indique que le defer est alloué sur le tas. Lorsqu'il vaut false, l'allocation se fait sur la pile. L'allocation sur la pile est automatiquement libérée au retour, ce qui est plus efficace en termes de gestion mémoire que l'allocation sur le tas. Le facteur déterminant l'allocation sur la pile est le nombre de boucles. Cette logique peut être retracée dans la méthode escape.goDeferStmt dans 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 représente le nombre de boucles dans lequel l'instruction courante se trouve. Si l'instruction defer courante n'est pas dans une boucle, elle sera allouée sur la pile.
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)
}Si l'allocation se fait sur la pile, la structure defer est créée directement sur la pile, et c'est la fonction runtime.deferprocStack qui termine la création de la structure defer.
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))
callArgs = append(callArgs, addr, s.mem())
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer argLa signature de la fonction deferprocStack est :
func deferprocStack(d *_defer)Sa logique de création spécifique n'est pas très différente de deferproc. La principale différence est que pour l'allocation sur la pile, la source de la structure defer est une structure créée directement, tandis que pour l'allocation sur le tas, la source est la fonction new.
Exécution
Lorsqu'une fonction est sur le point de retourner ou qu'un panic se produit, on entre dans la fonction runtime.deferreturn, qui est responsable de retirer le defer de la liste chaînée de la goroutine et de l'exécuter.
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()
}
}D'abord, on obtient le pointeur de pile de la fonction courante via getcallersp() et on le compare avec le sp dans la structure defer pour déterminer si le defer appartient à la fonction courante. Ensuite, on retire la structure defer de la tête de la liste chaînée, on exécute le prochain defer avec gp._defer = d.link, puis on libère la structure defer vers le pool via la fonction runtime.freedefer, et enfin on appelle fn pour l'exécuter. Cette boucle continue jusqu'à ce que tous les defer appartenant à la fonction courante soient exécutés.
Open-coded defer
L'utilisation de defer n'est pas sans coût. Bien qu'il offre une commodité syntaxique, ce n'est pas un appel de fonction direct et passe par une série de processus, ce qui entraîne une perte de performance. C'est pourquoi l'équipe Go a conçu une méthode d'optimisation : l'open-coded defer. C'est une méthode d'optimisation pour defer, dont le nom original en anglais est "open-coded", généralement traduit par "开放编码" en chinois. Ici, "open" signifie "déplier", c'est-à-dire déplier le code de la fonction defer dans le code de la fonction courante, comme l'inlining de fonction. Cette méthode d'optimisation a les conditions restrictives suivantes :
- Le nombre de
deferdans la fonction ne peut pas dépasser 8 - Le produit du nombre de
deferet du nombre dereturnne peut pas dépasser 15 - Le
deferne peut pas apparaître dans une boucle - L'optimisation du compilateur n'est pas désactivée
- Pas d'appel manuel à
os.Exit() - Pas besoin de copier les arguments depuis le tas
Cette logique de jugement peut être retracée dans la fonction 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
}Ensuite, Go crée une variable entière 8 bits deferBits dans la fonction courante pour servir de bitmap afin de marquer les defer. Chaque bit marque un defer, un entier 8 bits uint8 peut en représenter jusqu'à 8. Si le bit correspondant vaut 1, le defer optimisé open-coded sera exécuté lorsque la fonction est sur le point de retourner.
