memory
Contrairement au C/C++ traditionnel, Go est un langage avec GC. Dans la plupart des cas, l'allocation et la libération de la mémoire sont gérées automatiquement par Go. La décision d'allouer un objet sur la pile ou le tas est prise par le compilateur, essentiellement sans participation de l'utilisateur. Ce que l'utilisateur doit faire est simplement d'utiliser la mémoire. Dans Go, la gestion de la mémoire tas comporte deux grands composants : l'allocateur mémoire responsable de l'allocation de la mémoire tas, et le ramasse-miettes responsable de la récupération et libération de la mémoire tas inutile. Cet article explique principalement le fonctionnement de l'allocateur mémoire, qui est largement influencé par l'allocateur mémoire TCMalloc de Google.
Allocateur
Dans Go, il existe deux types d'allocateurs mémoire : l'allocateur linéaire et l'allocateur chaîné.
Allocation linéaire
L'allocateur linéaire correspond à la structure runtime.linearAlloc, comme montré ci-dessous.
type linearAlloc struct {
next uintptr // next free byte
mapped uintptr // one byte past end of mapped space
end uintptr // end of reserved space
mapMemory bool // transition memory from Reserved to Ready if true
}Cet allocateur pré-demande au système d'exploitation un espace mémoire continu. next pointe vers l'adresse mémoire disponible, end pointe vers l'adresse de fin de l'espace mémoire.
L'avantage de cette méthode d'allocation est sa rapidité et sa simplicité. L'inconvénient est assez évident : impossible de réutiliser la mémoire libérée, car next pointe uniquement vers l'espace mémoire restant, sans感知 l'espace mémoire précédemment utilisé puis libéré, ce qui entraîne un grand gaspillage d'espace mémoire.
L'allocation linéaire n'est donc pas la méthode d'allocation principale dans Go, elle n'est utilisée que sur les machines 32 bits comme fonction de pré-allocation mémoire.
Allocation chaînée
L'allocateur chaîné correspond à la structure runtime.fixalloc. La mémoire allouée par l'allocateur chaîné n'est pas continue, elle existe sous forme de liste simplement chaînée. L'allocateur chaîné est composé de plusieurs blocs mémoire de taille fixe, et chaque bloc mémoire est composé de plusieurs tranches mémoire de taille fixe. Chaque fois qu'une allocation mémoire est effectuée, une tranche mémoire de taille fixe est utilisée.
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // called first time p is returned
arg unsafe.Pointer
list *mlink
chunk uintptr // use uintptr instead of unsafe.Pointer to avoid write barriers
nchunk uint32 // bytes remaining in current chunk
nalloc uint32 // size of new chunks in bytes
inuse uintptr // in-use bytes now
stat *sysMemStat
zero bool // zero allocations
}L'avantage de l'allocateur chaîné est qu'il peut réutiliser la mémoire libérée. L'unité de base de la réutilisation de mémoire est une tranche mémoire de taille fixe, dont la taille est déterminée par fixalloc.size. Lors de la libération de mémoire, l'allocateur chaîné ajoute cette tranche mémoire comme nœud de tête à la liste des tranches mémoire libres.
Composants mémoire
L'allocateur mémoire de Go est principalement composé de mspan, heaparena, mcache, mcentral, mheap qui travaillent ensemble pour gérer toute la mémoire tas de Go.
mspan
runtime.mspan est l'unité de base de l'allocation mémoire Go. Chaque mspan gère mspan.npages pages mémoire de taille runtime.pageSize, généralement 8KB par page.
spanClass décide de la taille des éléments d'un mspan. Il y a 68 valeurs différentes, toutes stockées sous forme de table dans le fichier runtime.sizeclasses.go.
heaparena
Chaque heaparena gère plusieurs pages. La taille d'un heaparena est déterminée par runtime.heapArenaBytes, généralement 64MB. bitmap identifie si l'adresse correspondante dans la page contient un objet, zeroedBase est l'adresse de début de la mémoire de page gérée par ce heaparena, et spans enregistre quel mspan utilise chaque page.
mcache
mcache correspond à la structure runtime.mcache. Bien que son nom soit mcache, elle est en fait liée au processeur P. mcache est le cache mémoire de chaque processeur P, contenant un tableau de listes mspan alloc de taille fixe 136.
L'avantage d'utiliser mcache est qu'aucun verrou global n'est nécessaire lors de l'allocation mémoire. Cependant, quand sa mémoire est insuffisante, il faut accéder à mcentral, ce qui nécessite toujours un verrou.
mcentral
runtime.mcentral gère tous les mspan contenant des petits objets dans le tas. mcentral est directement géré par le tas mheap, il y a 136 mcentral au total dans le runtime.
mcentral est principalement responsable de deux tâches : allouer des mspan disponibles à mcache quand la mémoire est suffisante, et demander un nouveau mspan à mheap quand la mémoire est insuffisante.
mheap
runtime.mheap est le gestionnaire de la mémoire tas du langage Go. Dans le runtime, il existe comme variable globale runtime.mheap_.
Il gère tous les mspan créés, tous les mcentral, tous les heaparena, et de nombreux autres allocateurs.
Pour mheap, il y a principalement quatre tâches dans le runtime :
- Initialiser le tas
- Allouer des
mspan - Libérer des
mspan - Étendre le tas
Allocation d'objets
Go divise l'allocation mémoire d'objets en trois types différents selon leur taille :
- Micro-objet (tiny) : inférieur à 16B
- Petit objet (small) : inférieur à 32KB
- Grand objet (large) : supérieur à 32KB
Selon les trois types différents, une logique différente est exécutée lors de l'allocation mémoire. La fonction responsable de l'allocation mémoire pour les objets est runtime.mallocgc.
Micro-objets
Tous les micro-objets non-pointeurs de moins de 16B sont alloués dans un espace mémoire continu par le micro-allocateur de P. Dans runtime.mcache, le champ tiny enregistre l'adresse de base de cet espace mémoire.
Petits objets
La plupart des objets dans le runtime Go sont des petits objets dans la plage [16B, 32KB]. Le processus d'allocation des petits objets est le plus complexe.
L'allocation mémoire des petits objets descend niveau par niveau : d'abord mcache, puis mcentral, enfin mheap. L'allocation par mcache a le coût le plus bas car c'est un cache local de P, aucun verrou n'est nécessaire pour allouer de la mémoire. mcentral vient ensuite, et demander directement au mheap a le coût le plus élevé car la méthode mheap.alloc entre en concurrence pour le verrou global du tas entier.
Grands objets
L'allocation des grands objets est la plus simple. Si la taille de l'objet dépasse 32KB, une demande directe est faite à mheap pour allouer un nouveau mspan pour le contenir.
Autres
Statistiques mémoire
Le runtime Go expose une fonction ReadMemStats à l'utilisateur, qui peut être utilisée pour obtenir des statistiques sur la mémoire du runtime.
Cependant, son utilisation a un coût très élevé. Comme on peut le voir dans le code, l'analyse de la mémoire nécessite un STW avant de commencer, et la durée du STW peut aller de quelques millisecondes à plusieurs centaines de millisecondes. Elle n'est généralement utilisée que pour le débogage et la résolution de problèmes.
NotInHeap
L'allocateur mémoire est évidemment utilisé pour allouer la mémoire tas, mais le tas est divisé en deux parties : une partie pour la mémoire tas nécessaire au runtime Go lui-même, et l'autre pour la mémoire tas ouverte aux utilisateurs.
Donc dans certaines structures, on peut voir ce type de champ intégré :
_ sys.NotInHeapCela indique que la mémoire de ce type ne sera pas allouée sur le tas utilisateur. La vraie fonction de sys.NotInHeap est d'éviter les barrières mémoire pour améliorer l'efficacité du runtime, tandis que le tas utilisateur a besoin d'exécuter le GC donc nécessite des barrières mémoire.
