Maison > développement back-end > Golang > le corps du texte

Aller : Pointeurs et gestion de la mémoire

Patricia Arquette
Libérer: 2024-11-22 01:51:14
original
439 Les gens l'ont consulté

Go: Pointers & Memory Management

TL;DR : Explorez la gestion de la mémoire de Go avec des pointeurs, des allocations de pile et de tas, une analyse d'échappement et un garbage collection avec des exemples

Quand j'ai commencé à apprendre Go, j'ai été intrigué par son approche de la gestion de la mémoire, notamment en ce qui concerne les pointeurs. Go gère la mémoire d'une manière à la fois efficace et sûre, mais cela peut être un peu une boîte noire si vous ne regardez pas sous le capot. Je souhaite partager quelques informations sur la façon dont Go gère la mémoire avec des pointeurs, la pile et le tas, ainsi que des concepts tels que l'analyse d'échappement et le garbage collection. En cours de route, nous examinerons des exemples de code qui illustrent ces idées dans la pratique.

Comprendre la mémoire de pile et de tas

Avant de plonger dans les pointeurs dans Go, il est utile de comprendre comment fonctionnent la pile et le tas. Ce sont deux zones de mémoire où peuvent être stockées des variables, chacune avec ses propres caractéristiques.

  • Pile : Il s'agit d'une région de mémoire qui fonctionne selon le principe du dernier entré, premier sorti. C'est rapide et efficace, utilisé pour stocker des variables avec une portée de courte durée, comme des variables locales dans des fonctions.
  • Heap : il s'agit d'un plus grand pool de mémoire utilisé pour les variables qui doivent vivre au-delà de la portée d'une fonction, telles que les données renvoyées par une fonction et utilisées ailleurs.

Dans Go, le compilateur décide d'allouer les variables sur la pile ou sur le tas en fonction de la façon dont elles sont utilisées. Ce processus de prise de décision est appelé analyse d'évasion, que nous explorerons plus en détail plus tard.

Passage par valeur : le comportement par défaut

Dans Go, lorsque vous transmettez des variables comme un entier, une chaîne ou un booléen à une fonction, elles sont naturellement transmises par valeur. Cela signifie qu'une copie de la variable est effectuée et que la fonction fonctionne avec cette copie. Cela signifie que toute modification apportée à la variable à l'intérieur de la fonction n'affectera pas la variable en dehors de sa portée.

Voici un exemple simple :

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Sortie :

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Dans ce code :

  • La fonction incrément() reçoit une copie de n.
  • Les adresses de n dans main() et num dans incrément() sont différentes.
  • Modifier num à l'intérieur d'incrément() n'affecte pas n dans main().

À retenir : Le passage par valeur est sûr et simple, mais pour les grandes structures de données, la copie peut devenir inefficace.

Présentation des pointeurs : passage par référence

Pour modifier la variable d'origine à l'intérieur d'une fonction, vous pouvez lui passer un pointeur. Un pointeur contient l'adresse mémoire d'une variable, permettant aux fonctions d'accéder et de modifier les données d'origine.

Voici comment utiliser les pointeurs :

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Sortie :

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
Copier après la connexion
Copier après la connexion

Dans cet exemple :

  • Nous transmettons l'adresse de n à IncreasePointer().
  • Main() et IncreasePointer() font tous deux référence à la même adresse mémoire.
  • La modification de num à l'intérieur d'incrémentPointer() affecte n dans main().

À retenir : L'utilisation de pointeurs permet aux fonctions de modifier la variable d'origine, mais elle introduit des considérations sur l'allocation de mémoire.

Allocation de mémoire avec des pointeurs

Lorsque vous créez un pointeur vers une variable, Go doit s'assurer que la variable dure aussi longtemps que le pointeur. Cela signifie souvent allouer la variable sur le tas plutôt que sur la pile.

Considérez cette fonction :

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Ici, num est une variable locale dans createPointer(). Si num était stocké sur la pile, il serait nettoyé une fois la fonction renvoyée, laissant un pointeur pendant. Pour éviter cela, Go alloue num sur le tas afin qu'il reste valide après la sortie de createPointer().

Pointeurs pendants

Un pointeur suspendu se produit lorsqu'un pointeur fait référence à une mémoire qui a déjà été libérée.

Go évite les pointeurs suspendus avec son garbage collector, garantissant que la mémoire n'est pas libérée alors qu'elle est encore référencée. Cependant, conserver les pointeurs plus longtemps que nécessaire peut entraîner une utilisation accrue de la mémoire ou des fuites de mémoire dans certains scénarios.

Analyse d'évasion : décider de l'allocation de la pile par rapport à l'allocation du tas

L'analyse d'évasion détermine si les variables doivent vivre au-delà de la portée de leur fonction. Si une variable est renvoyée, stockée dans un pointeur ou capturée par une goroutine, elle s'échappe et est allouée sur le tas. Cependant, même si une variable ne s'échappe pas, le compilateur peut l'allouer sur le tas pour d'autres raisons, telles que des décisions d'optimisation ou des limitations de taille de pile.

Exemple d'échappement de variable :

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Dans ce code :

  • Les données de tranche dans createSlice() s'échappent car elles sont renvoyées et utilisées dans main().
  • Le tableau sous-jacent de la tranche est alloué sur le heap.

Comprendre l'analyse d'échappement avec go build -gcflags '-m'

Vous pouvez voir ce que décide le compilateur de Go en utilisant l'option -gcflags '-m' :

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cela affichera des messages indiquant si les variables s'échappent vers le tas.

Collecte des déchets à Go

Go utilise un garbage collector pour gérer l'allocation et la désallocation de mémoire sur le tas. Il libère automatiquement la mémoire qui n'est plus référencée, aidant ainsi à prévenir les fuites de mémoire.

Exemple :

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
Copier après la connexion
Copier après la connexion

Dans ce code :

  • Nous créons une liste chaînée avec 1 000 000 de nœuds.
  • Chaque nœud est alloué sur le tas car il échappe à la portée de createLinkedList().
  • Le garbage collector libère la mémoire lorsque la liste n'est plus nécessaire.

À retenir : Le garbage collector de Go simplifie la gestion de la mémoire mais peut introduire une surcharge.

Pièges potentiels avec les pointeurs

Bien que les pointeurs soient puissants, ils peuvent entraîner des problèmes s'ils ne sont pas utilisés avec précaution.

Pointeurs pendants (suite)

Bien que le garbage collector de Go aide à éviter les pointeurs suspendus, vous pouvez toujours rencontrer des problèmes si vous conservez les pointeurs plus longtemps que nécessaire.

Exemple :

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Dans ce code :

  • les données sont une grande tranche allouée sur le tas.
  • En gardant une référence à celui-ci ([]int), on empêche le garbage collector de libérer la mémoire.
  • Cela peut entraîner une utilisation accrue de la mémoire si elle n'est pas gérée correctement.

Problèmes de concurrence – Course aux données avec des pointeurs

Voici un exemple où les pointeurs sont directement impliqués :

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Pourquoi ce code échoue :

  • Plusieurs goroutines déréférencent et incrémentent le compteur de pointeurPtr sans aucune synchronisation.
  • Cela conduit à une course aux données car plusieurs goroutines accèdent et modifient simultanément le même emplacement mémoire sans synchronisation. L'opération *counterPtr implique plusieurs étapes (lecture, incrémentation, écriture) et n'est pas thread-safe.

Réparer la course aux données :

Nous pouvons résoudre ce problème en ajoutant une synchronisation avec un mutex :

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Comment fonctionne ce correctif :

  • Les mu.Lock() et mu.Unlock() garantissent qu'une seule goroutine accède et modifie le pointeur à la fois.
  • Cela évite les conditions de concurrence et garantit que la valeur finale du compteur est correcte.

Que dit la spécification linguistique de Go ?

Il convient de noter que La spécification du langage Go ne dicte pas directement si les variables sont allouées sur la pile ou sur le tas. Il s'agit de détails d'implémentation du runtime et du compilateur, permettant une flexibilité et des optimisations qui peuvent varier selon les versions ou implémentations de Go.

Cela signifie :

  • La façon dont la mémoire est gérée peut changer entre les différentes versions de Go.
  • Vous ne devriez pas compter sur l'allocation de variables dans une zone spécifique de la mémoire.
  • Concentrez-vous sur l'écriture d'un code clair et correct plutôt que d'essayer de contrôler l'allocation de mémoire.

Exemple :

Même si vous vous attendez à ce qu'une variable soit allouée sur la pile, le compilateur peut décider de la déplacer vers le tas en fonction de son analyse.

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

À retenir : Comme les détails d'allocation de mémoire sont une sorte d'implémentation interne et ne font pas partie de la spécification du langage Go, ces informations ne sont que des directives générales et non des règles fixes qui pourraient changer à une date ultérieure.

Équilibrer les performances et l’utilisation de la mémoire

Lors du choix entre le passage par valeur ou par pointeur, nous devons prendre en compte la taille des données et les implications en termes de performances.

Passage de grandes structures par valeur :

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Passage de grandes structures par pointeur :

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Considérations :

  • Le passage par valeur est sûr et simple, mais peut s'avérer inefficace pour les grandes structures de données.
  • Le passage par pointeur évite la copie mais nécessite une manipulation minutieuse pour éviter les problèmes de concurrence.

De l'expérience de terrain :

En début de carrière, je me souviens d'une époque où j'optimisais une application Go qui traitait de grands ensembles de données. Au départ, j'ai transmis de grandes structures par valeur, en supposant que cela simplifierait le raisonnement sur le code. Cependant, il m'est arrivé de remarquer une utilisation de la mémoire relativement élevée et des pauses fréquentes dans le ramasse-miettes.

Après avoir profilé l'application à l'aide de l'outil pprof de Go en programmation en binôme avec mon senior, nous avons constaté que la copie de structures volumineuses constituait un goulot d'étranglement. Nous avons refactorisé le code pour transmettre des pointeurs au lieu de valeurs. Cela a réduit l'utilisation de la mémoire et amélioré considérablement les performances.

Mais le changement ne s’est pas fait sans défis. Nous devions nous assurer que notre code était thread-safe puisque plusieurs goroutines accédaient désormais aux données partagées. Nous avons implémenté la synchronisation à l'aide de mutex et examiné attentivement le code pour détecter les conditions de concurrence potentielles.

Leçon apprise : Comprendre très tôt comment Go gère l'allocation de mémoire peut vous aider à écrire du code plus efficace, car il est essentiel d'équilibrer les gains de performances avec la sécurité et la maintenabilité du code.

Pensées finales

L'approche de Go en matière de gestion de la mémoire (comme partout ailleurs) établit un équilibre entre performances et simplicité. En supprimant de nombreux détails de bas niveau, il permet aux développeurs de se concentrer sur la création d'applications robustes sans s'enliser dans la gestion manuelle de la mémoire.

Points clés à retenir :

  • Le passage par valeur est simple mais peut s'avérer inefficace pour les grandes structures de données.
  • L'utilisation de pointeurs peut améliorer les performances mais nécessite une manipulation prudente pour éviter des problèmes tels que des courses de données.
  • L'analyse d'échappement détermine si les variables sont allouées sur la pile ou sur le tas, mais il s'agit d'un détail interne.
  • Le garbage collection aide à prévenir les fuites de mémoire, mais peut entraîner une surcharge.
  • La Concurrency nécessite une synchronisation lorsque des données partagées sont impliquées.

En gardant ces concepts à l'esprit et en utilisant les outils de Go pour profiler et analyser votre code, vous pouvez écrire des applications efficaces et sûres.


J'espère que cette exploration de la gestion de la mémoire de Go avec des pointeurs sera utile. Que vous débutiez avec Go ou que vous cherchiez à approfondir vos connaissances, expérimenter le code et observer le comportement du compilateur et du runtime est un excellent moyen d'apprendre.

N'hésitez pas à partager vos expériences ou toutes vos questions — j'ai toujours envie de discuter, d'apprendre et d'écrire davantage sur Go !

Contenu bonus - Prise en charge du pointeur direct

Tu sais ? Les pointeurs peuvent être directement créés pour certains types de données et ne le peuvent pas pour certains. Ce petit tableau les couvre.


Type Supports Direct Pointer Creation? Example
Structs ✅ Yes p := &Person{Name: "Alice", Age: 30}
Arrays ✅ Yes arrPtr := &[3]int{1, 2, 3}
Slices ❌ No (indirect via variable) slice := []int{1, 2, 3}; slicePtr := &slice
Maps ❌ No (indirect via variable) m := map[string]int{}; mPtr := &m
Channels ❌ No (indirect via variable) ch := make(chan int); chPtr := &ch
Basic Types ❌ No (requires a variable) val := 42; p := &val
time.Time (Struct) ✅ Yes t := &time.Time{}
Custom Structs ✅ Yes point := &Point{X: 1, Y: 2}
Interface Types ✅ Yes (but rarely needed) var iface interface{} = "hello"; ifacePtr := &iface
time.Duration (Alias of int64) ❌ No duration := time.Duration(5); p := &duration
Tapez Prend en charge la création de pointeurs directs ? Exemple
ête> Structures ✅ Oui p := &Person{Nom : "Alice", Âge : 30 Tableaux ✅ Oui arrPtr := &[3]int{1, 2, 3} Tranches ❌ Non (indirect via variable) slice := []int{1, 2, 3}; slicePtr := &slice Cartes ❌ Non (indirect via variable) m := map[string]int{}; mPtr := &m Chaînes ❌ Non (indirect via variable) ch := make(chan int); chPtr := &ch Types de base ❌ Non (nécessite une variable) val := 42; p := &val time.Time (Struct) ✅ Oui t := &time.Time{} Structures personnalisées ✅ Oui point := &Point{X : 1, Y : 2} Types d'interfaces ✅ Oui (mais rarement nécessaire) var iface interface{} = "bonjour"; ifacePtr := &iface time.Duration (Alias ​​de int64) ❌ Non durée := time.Duration(5); p := &durée

S'il vous plaît laissez-moi savoir dans les commentaires si vous aimez ça ; J'essaierai d'ajouter de tels contenus bonus à mes articles à l'avenir.

Merci d'avoir lu ! Pour plus de contenu, pensez à suivre.

Que le code soit avec vous :)

Mes liens sociaux : LinkedIn | GitHub | ? (anciennement Twitter) | Sous-pile | Dev.to | Hashnode

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!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal