Skip to content

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.

go
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 :

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
}

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.

go
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 :

go
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 :

go
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 :

go
defer fn1(x, y)

Après compilation, le code réel est :

go
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 :

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

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.

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

Elle remplit d'abord la moitié du deferpool local depuis le sched.deferpool global, puis tente d'obtenir depuis le deferpool de 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

Enfin, si aucune structure n'est trouvée, une allocation manuelle est effectuée. On peut voir ce code :

go
d.heap = true

Cela 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 :

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 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.

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

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.

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))
    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 arg

La signature de la fonction deferprocStack est :

go
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.

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

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 :

  1. Le nombre de defer dans la fonction ne peut pas dépasser 8
  2. Le produit du nombre de defer et du nombre de return ne peut pas dépasser 15
  3. Le defer ne peut pas apparaître dans une boucle
  4. L'optimisation du compilateur n'est pas désactivée
  5. Pas d'appel manuel à os.Exit()
  6. Pas besoin de copier les arguments depuis le tas

Cette logique de jugement peut être retracée dans la fonction 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
}

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.

Golang by www.golangdev.cn edit