Maison  >  Article  >  développement back-end  >  Un article explique l'allocation de mémoire dans Go

Un article explique l'allocation de mémoire dans Go

Go语言进阶学习
Go语言进阶学习avant
2023-07-25 13:57:09967parcourir

Aujourd'hui, je vais partager avec vous quelques points de connaissances communs sur la gestion de la mémoire dans Go.

# 1. Trois composants majeurs de l'allocation de mémoire

Go Le processus d'allocation de mémoire est principalement géré par trois composants majeurs. Les niveaux de haut en bas sont :

mheap

Go. Au démarrage du programme, il demandera d'abord un gros bloc de mémoire au système d'exploitation et le laissera gérer globalement par la structure mheap.

Comment le gérer spécifiquement ? mheap divisera ce grand morceau de mémoire en petits blocs de mémoire de spécifications différentes, que nous appelons mspan. Selon la taille de la spécification, il existe environ 70 types de mspan. La division peut être considérée comme très fine, suffisante pour se rencontrer. les besoins de diverses distributions de mémoires d'objets.

Donc ces spécifications mspan de différentes tailles, mélangées, doivent être difficiles à gérer, non ?

Il y a donc le composant de niveau supérieur mcentral

mcentral

Le démarrage d'un programme Go initialisera de nombreux mcentrals, et chaque mcentral est uniquement responsable de la gestion des mspans d'une spécification spécifique.

Équivalent à mcentral, qui met en œuvre une gestion raffinée de mspan basée sur mheap.

Mais mcentral est globalement visible dans le programme Go, donc chaque fois que la coroutine vient à mcentral pour demander de la mémoire, elle doit être verrouillée.

On peut s'attendre à ce que si chaque coroutine vient à mcentral pour demander de la mémoire, la surcharge liée au verrouillage et à la libération fréquents sera très importante.

Par conséquent, un proxy secondaire de mcentral est nécessaire pour amortir cette pression

mcache

Dans un programme Go, chaque threadM sera lié à un processeurP, une seule exécution multi-traitement peut être effectuée à une seule granularité de tempsgoroutine, chaqueP sera lié Un est appelé le cache local de mcache. M会绑定给一个处理器P,在单一粒度的时间里只能做多处理运行一个goroutine,每个P都会绑定一个叫 mcache 的本地缓存。

当需要进行内存分配时,当前运行的goroutine会从mcache中查找可用的mspan。从本地mcache

Lorsqu'une allocation de mémoire est requise, le goroutine commencera à partir de Trouver disponible <code style="font-size: hériter;line-height: hériter;overflow-wrap: break-word;padding: 2px in mcache 4px ;border-rayon : 4px;marge-droite : 2px;marge-gauche : 2px;couleur : rgb(226, 36, 70);arrière-plan : rgb(248, 248, 248);">mspan. De localIl n'est pas nécessaire de verrouiller lors de l'allocation de mémoire dans mcache. Cette stratégie d'allocation est plus efficace.

chaîne d'approvisionnement mspan

Le nombre de mspans dans mcentral n'est pas toujours suffisant lorsque l'offre dépasse la demande, mcache demandera à nouveau plus de mspans à mcentral. De même, si le nombre de mspans dans mcentral n'est pas suffisant, mcentral. appliquera également mspan à partir de son mheap supérieur. Pour être plus extrême, que devons-nous faire si le mspan dans mheap ne peut pas satisfaire la demande de mémoire du programme ?

Alors il n'y a pas d'autre moyen, mheap ne peut s'appliquer sans vergogne qu'au grand frère du système d'exploitation.

Un article explique l'allocation de mémoire dans Go

🎜

Le processus de fourniture ci-dessus s'applique uniquement aux scénarios dans lesquels le bloc de mémoire est inférieur à 64 Ko. La raison en est que Go ne peut pas utiliser le cache local du thread de travail pour allouer le nombre correspondant de pages de mémoire (chaque taille de page est de 8 Ko) au. programme. mcache和全局中心缓存 mcentral 上管理超过 64KB 的内存分配,所以对于那些超过 64KB 的内存申请,会直接从堆上(mheap

# 2. Que sont la mémoire tas et la mémoire pile ?

Selon différentes méthodes de gestion de la mémoire (allocation et recyclage), la mémoire peut être divisée en

mémoire de tas et mémoire de pile.

Alors, quelle est la différence entre eux ?

Mémoire tas : l'allocateur de mémoire et le ramasse-miettes sont responsables du recyclage

Mémoire de pile : automatiquement allouée et libérée par le compilateur

Lorsqu'un programme est en cours d'exécution, il peut y avoir plusieurs mémoires de pile, mais il y en a certainement Il n'y aura qu'un seul tas de mémoire.

Chaque mémoire de pile est occupée indépendamment par un thread ou une coroutine, il n'est donc pas nécessaire de se verrouiller lors de l'allocation de mémoire de la pile, et la mémoire de la pile sera automatiquement recyclée après la fin de la fonction, et les performances sont supérieures à celles du tas mémoire.

Et qu'en est-il de la mémoire tas ? Étant donné que plusieurs threads ou coroutines peuvent demander de la mémoire au tas en même temps, la demande de mémoire dans le tas nécessite un verrouillage pour éviter les conflits, et la mémoire du tas nécessite l'intervention du GC (garbage collection) après la fin de la fonction. Un certain nombre d’opérations du GC dégraderont sérieusement la performance du programme.

# 3. La nécessité d'une analyse d'échappement

On peut voir que afin d'améliorer les performances du programme, l'allocation de mémoire sur le tas doit être minimisée, ce qui peut réduire la pression sur le GC.

Pour déterminer si une variable se voit allouer de la mémoire sur le tas ou sur la pile, bien que les prédécesseurs aient résumé certaines règles, il appartient au programmeur de toujours prêter attention à ce problème lors du codage, et les exigences pour les programmeurs sont assez élevées.

Heureusement, le compilateur Go ouvre également la fonction d'analyse d'échappement. Grâce à l'analyse d'échappement, vous pouvez détecter directement toutes les variables allouées sur le tas par votre programmeur (ce phénomène est appelé escape).

La méthode consiste à exécuter la commande suivante

go build -gcflags &#39;-m -l&#39; demo.go 

# 或者再加个 -m 查看更详细信息
go build -gcflags &#39;-m -m -l&#39; demo.go

# Les règles d'emplacement d'allocation de mémoire

Si vous utilisez un outil d'analyse d'échappement, vous pouvez en fait déterminer manuellement quelles variables sont allouées sur le tas.

Alors quelles sont ces règles ?

Après résumé, il existe principalement quatre situations comme suit

  1. Selon le périmètre d'utilisation de la variable

  2. Selon la détermination du type de variable

  3. Selon l'occupation taille de la variable

  4. Basé sur La longueur de la variable est-elle déterminée ? Ensuite, analysons et vérifions un par un

  5. Selon le champ d'utilisation de la variable

Lorsque vous compilez, le compilateur effectuera une analyse d'échappement (analyse d'échappement). Lorsqu'une variable est trouvée, la portée d'utilisation est uniquement dans la fonction, alors de la mémoire peut lui être allouée sur la pile.

Par exemple, dans l'exemple ci-dessous
func foo() int {
    v := 1024
    return v
}

func main() {
    m := foo()
    fmt.Println(m)
}

nous pouvons passer go build -gcflags '-m -l' demo.go pour afficher les résultats de l'analyse d'échappement, où-m permet d'imprimer les informations d'analyse d'échappement, -l désactive l'optimisation en ligne.

D'après les résultats de l'analyse, nous n'avons vu aucune instruction d'échappement concernant la variable v, indiquant qu'elle ne s'est pas échappée et a été allouée sur la pile.

$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:12:13: ... argument does not escape
./demo.go:12:13: m escapes to heap

Et si la variable doit être utilisée en dehors du cadre de la fonction, si elle est toujours allouée sur la pile, alors au retour de la fonction, l'espace mémoire pointé par la variable sera recyclé, et le programme le fera inévitablement signaler une erreur, donc pour ce type de variables, les variables ne peuvent être allouées que sur le tas.

go build -gcflags '-m -l' demo.go 来查看逃逸分析的结果,其中 -m 是打印逃逸分析的信息,-l 则是禁止内联优化。

从分析的结果我们并没有看到任何关于 v 变量的逃逸说明,说明其并没有逃逸,它是分配在栈上的。

func foo() *int {
    v := 1024
    return &v
}

func main() {
    m := foo()
    fmt.Println(*m) // 1024
}

而如果该变量还需要在函数范围之外使用,如果还在栈上分配,那么当函数返回的时候,该变量指向的内存空间就会被回收,程序势必会报错,因此对于这种变量只能在堆上分配。

比如下边这个例子,返回的是指针

$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:6:2: moved to heap: v
./demo.go:12:13: ... argument does not escape
./demo.go:12:14: *m escapes to heap

从逃逸分析的结果中可以看到 moved to heap: vPar exemple, dans l'exemple suivant, renvoie un pointeur

func foo() []int {
    a := []int{1,2,3}
    return a
}

func main() {
    b := foo()
    fmt.Println(b)
}
🎜 Échapper à Vous pouvez voir dans les résultats de l'analysedéplacé vers le tas : v, la variable v est la mémoire allouée à partir du tas , et Il existe des différences évidentes entre les scénarios ci-dessus. 🎜
$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:6:2: moved to heap: v
./demo.go:12:13: ... argument does not escape
./demo.go:12:14: *m escapes to heap

除了返回指针之外,还有其他的几种情况也可归为一类:

第一种情况:返回任意引用型的变量:Slice 和 Map

func foo() []int {
    a := []int{1,2,3}
    return a
}

func main() {
    b := foo()
    fmt.Println(b)
}

逃逸分析结果

$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:6:12: []int literal escapes to heap
./demo.go:12:13: ... argument does not escape
./demo.go:12:13: b escapes to heap

第二种情况:在闭包函数中使用外部变量

func Increase() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    in := Increase()
    fmt.Println(in()) // 1
    fmt.Println(in()) // 2
}

逃逸分析结果

$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:6:2: moved to heap: n
./demo.go:7:9: func literal escapes to heap
./demo.go:15:13: ... argument does not escape
./demo.go:15:16: in() escapes to heap

 根据变量类型是否确定

在上边例子中,也许你发现了,所有编译输出的最后一行中都是 m escapes to heap

奇怪了,为什么 m 会逃逸到堆上?

其实就是因为我们调用了 fmt.Println() 函数,它的定义如下

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

可见其接收的参数类型是 interface{} ,对于这种编译期不能确定其参数的具体类型,编译器会将其分配于堆上。

 根据变量的占用大小

最开始的时候,就介绍到,以 64KB 为分界线,我们将内存块分为 小内存块 和 大内存块。

小内存块走常规的 mspan 供应链申请,而大内存块则需要直接向 mheap,在堆区申请。

以下的例子来说明

func foo() {
    nums1 := make([]int, 8191) // < 64KB
    for i := 0; i < 8191; i++ {
        nums1[i] = i
    }
}

func bar() {
    nums2 := make([]int, 8192) // = 64KB
    for i := 0; i < 8192; i++ {
        nums2[i] = i
    }
}

-gcflags 多加个 -m 可以看到更详细的逃逸分析的结果

$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:5:15: make([]int, 8191) does not escape
./demo.go:12:15: make([]int, 8192) escapes to heap

那为什么是 64 KB 呢?

我只能说是试出来的 (8191刚好不逃逸,8192刚好逃逸),网上有很多文章千篇一律的说和  ulimit -a 中的 stack size 有关,但经过了解这个值表示的是系统栈的最大限制是 8192 KB,刚好是 8M。

$ ulimit -a
-t: cpu time (seconds)              unlimited
-f: file size (blocks)              unlimited
-d: data seg size (kbytes)          unlimited
-s: stack size (kbytes)             8192

我个人实在无法理解这个 8192 (8M) 和 64 KB 是如何对应上的,如果有朋友知道,还请指教一下。

 根据变量长度是否确定

由于逃逸分析是在编译期就运行的,而不是在运行时运行的。因此避免有一些不定长的变量可能会很大,而在栈上分配内存失败,Go 会选择把这些变量统一在堆上申请内存,这是一种可以理解的保险的做法。

func foo() {
    length := 10
    arr := make([]int, 0 ,length)  // 由于容量是变量,因此不确定,因此在堆上申请
}

func bar() {
    arr := make([]int, 0 ,10)  // 由于容量是常量,因此是确定的,因此在栈上申请
}

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