Maison  >  Article  >  développement back-end  >  Parlons de l'analyse de l'évasion de Golang

Parlons de l'analyse de l'évasion de Golang

藏色散人
藏色散人avant
2021-07-01 14:56:152192parcourir

Traduit de : http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html

Le garbage collection est une fonctionnalité très pratique de Go : sa gestion automatique de la mémoire rend le code plus propre tout en réduisant les risques de fuites de mémoire. Cependant, étant donné que le garbage collection nécessite un arrêt périodique du programme pour collecter les objets inutilisés, une surcharge supplémentaire sera inévitablement ajoutée. Le compilateur Go est intelligent et décidera automatiquement si une variable doit être allouée sur le tas pour une collecte facile à l'avenir, ou allouée directement à l'espace de pile de la fonction. Pour les variables allouées sur la pile, la différence avec les variables allouées sur le tas est qu'au retour de la fonction, l'espace de la pile sera détruit, de sorte que les variables sur la pile sont directement détruites sans surcharge supplémentaire de garbage collection.

L'analyse d'évasion de Go est plus basique que HotSpot de la machine virtuelle Java. La règle de base est que si une référence à une variable est renvoyée par la fonction dans laquelle elle est déclarée, un "échappement" se produit. Parce qu'elle peut être utilisée par un autre contenu en dehors de la fonction, elle doit être allouée sur le tas. Les situations suivantes seront plus compliquées :

  • Fonctions appelant d'autres fonctions
  • Référencement des variables membres en tant que structures
  • Découpage et mappage
  • Pointeurs Cgo vers des variables

Afin d'implémenter l'analyse d'échappement, Go construira pendant la phase de compilation Diagramme d'appel de fonction, tout en suivant le processus des paramètres d'entrée et des valeurs de retour. Si une fonction fait uniquement référence à un paramètre, mais que la référence ne renvoie pas la fonction, la variable ne s'échappera pas. Si une fonction renvoie une référence, mais que la référence est libérée par une autre fonction sur la pile ou ne renvoie pas la référence, il n'y a pas d'échappement. Afin de démontrer plusieurs exemples, vous pouvez ajouter le paramètre -gcflags '-m' lors de la compilation. Ce paramètre imprimera les informations détaillées de l'analyse d'échappement :

package main

type S struct {}

func main() {
    var x S
    _ = identity(x)
}

func identity(x S) S {
    return x
}

Vous pouvez exécuter go run -gcflags '-m -l' (Remarque : le nom du fichier de code go est omis dans le texte original) pour compiler ce code, paramètre -l C'est pour empêcher la fonction identity d'être inline (nous discuterons du sujet de l'inline une autre fois). Vous ne verrez aucune sortie ! Go utilise le passage de valeurs, donc la variable main dans la fonction x sera toujours copiée dans l'espace de pile de la fonction identity. Habituellement, le code qui n'utilise pas de références alloue de la mémoire via l'espace de pile. Aucune analyse d’évasion n’est donc impliquée. Essayons la plus difficile :

package main

type S struct {}

func main() {
    var x S
    y := &x
    _ = *identity(y)
}

func identity(z *S) *S {
    return z
}

La sortie correspondante est :

./escape.go:11: leaking param: z to result ~r1
./escape.go:7: main &x does not escape

La première ligne montre le "flux" de la variable z : le paramètre d'entrée est renvoyé directement comme valeur de retour. Mais la fonction identity n'a pas supprimé la référence z, donc aucun échappement de variable ne s'est produit. Après le retour de la fonction main, aucune référence à x n'existe, donc la variable x peut se voir allouer de la mémoire dans l'espace de pile de la fonction main.
La troisième expérience :

package main

type S struct {}

func main() {
  var x S
  _ = *ref(x)
}

func ref(z S) *S {
  return &z
}

Le résultat est :

./escape.go:10: moved to heap: z
./escape.go:11: &z escapes to heap

Maintenant, il y a une évasion. N'oubliez pas que Go est un passage par valeur, donc z est une copie de la variable x. La fonction ref renvoie une référence à z, donc z ne peut pas être alloué sur la pile, sinon lorsque la fonction ref reviendra, où sera le point de référence ? Alors il s'est échappé dans le tas. En fait, après avoir exécuté ref et revenu à la fonction main, la fonction main supprime la référence au lieu de la déréférencer, mais l'analyse d'échappement de Go n'est pas assez intelligente pour identifier cette situation.
Il est à noter que dans ce cas, si nous n'arrêtons pas la référence, le compilateur sera intégré ref.
Que se passe-t-il si les membres de la structure sont définis comme références ?

package main

type S struct {
  M *int
}

func main() {
  var i int
  refStruct(i)
}

func refStruct(y int) (z S) {
  z.M = &y
  return z
}

Le résultat est :

./escape.go:12: moved to heap: y
./escape.go:13: &y escapes to heap

Dans ce cas, Go suit toujours le flux de la référence même si elle est membre de la structure. Puisque la fonction refStruct prend une référence et la renvoie, y doit s'échapper. Comparez l'exemple suivant :

package main

type S struct {
  M *int
}

func main() {
  var i int
  refStruct(&i)
}

func refStruct(y *int) (z S) {
  z.M = y
  return z
}

Le résultat est :

./escape.go:12: leaking param: y to result z
./escape.go:9: main &i does not escape

Bien que la variable main soit référencée dans la fonction i et transmise à la fonction refStruct, la portée de cette référence ne dépasse pas l'espace de pile dans lequel elle est déclarée. Ceci est légèrement différent du programme précédent en sémantique, et celui-ci sera plus efficace : dans le programme précédent, la variable i doit être allouée sur la pile de la fonction main, puis copiée dans la fonction refStruct en paramètre, et la copie allouée sur le tas. Dans cet exemple, i n'est attribué qu'une seule fois, puis la référence est transmise.

Regardons un exemple quelque peu alambiqué :

package main

type S struct {
  M *int
}

func main() {
  var x S
  var i int
  ref(&i, &x)
}

func ref(y *int, z *S) {
  z.M = y
}

Le résultat est :

./escape.go:13: leaking param: y
./escape.go:13: ref z does not escape
./escape.go:9: moved to heap: i
./escape.go:10: &i escapes to heap
./escape.go:10: main &x does not escape

问题在于,y被赋值给了一个入参结构体的成员。Go并不能追溯这种关系(go只能追溯输入直接流向输出),所以逃逸分析失败了,所以变量只能分配到堆上。由于Go的逃逸分析的局限性,许多变量会被分配到堆上,请参考此链接,这里面记录了许多案例(从Go1.5开始)。

最后,来看下映射和切片是怎样的呢?请记住,切片和映射实际上只是具有指向堆内存的指针的Go结构:slice结构是暴露在reflect包中(SliceHeader
)。map结构就更隐蔽了:存在于hmap。如果这些结构体不逃逸,将会被分配到栈上,但是其底层的数组或者哈希桶中的实际数据会被分配到堆上去。避免这种情况的唯一方法是分配一个固定大小的数组(例如[10000]int)。

如果你剖析过你的程序堆使用情况(https://blog.golang.org/pprof
),并且想减少垃圾回收的消耗,可以将频繁分配到堆上的变量移到栈上,可能会有较好的效果。进一步研究HotSpot JVM是如何进行逃逸分析的会是一个不错的话题,可以参考这个链接,这个里面主要讲解了栈分配,以及有关何时可以消除同步的检测。

更多golang相关技术文章,请访问golang教程栏目!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer