1 points par GN⁺ 2025-04-24 | 1 commentaires | Partager sur WhatsApp
  • Le langage Go présente très peu de comportements indéfinis et dispose d’une sémantique de GC (garbage collection) simple
  • En Go, la gestion manuelle de la mémoire est possible, et peut être mise en œuvre en coopération avec le GC
  • Une arena est une structure de données permettant d’allouer efficacement de la mémoire ayant la même durée de vie, et l’article explique comment l’implémenter en Go
  • Il explique comment le GC gère la mémoire via l’algorithme Mark and Sweep
  • L’utilisation d’une arena peut améliorer les performances d’allocation mémoire, grâce à diverses optimisations
  • Tentative d’amélioration des performances et de minimisation de la charge du GC via la suppression des write barriers, la réutilisation mémoire, le chunk pooling, etc.
  • Présentation de motifs sûrs et rapides pour le traitement réel de gros volumes mémoire, via des fonctionnalités comme l’implémentation de realloc, la réutilisation d’arena et sa réinitialisation (Reset)

Vue d’ensemble de l’allocation manuelle de mémoire basée sur une arena en Go

  • Go est un langage sûr grâce à un comportement de GC clair et à l’absence quasi totale d’Undefined Behavior
  • Le package unsafe permet un contrôle direct de la mémoire adapté à l’implémentation interne du GC
  • Cet article explique comment construire en Go un allocateur mémoire basé sur une structure d’arena capable de coopérer avec le GC

Définition et utilité d’une arena

  • Une arena est une structure conçue pour allouer efficacement des objets ayant la même durée de vie
  • Là où un append classique étend un tableau de façon exponentielle, une arena ajoute de nouveaux blocs et fournit des pointeurs
  • L’interface standard est la suivante :
    • Alloc(size, align uintptr) unsafe.Pointer

Fonctionnement des pointeurs et du GC

  • Le GC fonctionne en marquant (mark) puis en récupérant (sweep) la mémoire
  • Pour un GC précis, il utilise des métadonnées appelées pointer bits qui indiquent l’emplacement des pointeurs
  • Si les pointeurs sont mal gérés dans une arena, le GC peut ne pas les suivre, ce qui peut provoquer des erreurs de use-after-free

Méthode de conception d’une arena

  • La structure d’arena possède les champs suivants :
    • next, left, cap, chunks
  • Toutes les allocations sont alignées sur 8 octets, et si l’espace manque, un nouveau chunk est créé via nextPow2
  • Les chunks sont alloués non pas comme []uintptr, mais avec un type struct { A [N]uintptr; P *Arena }, afin que le GC puisse suivre l’arena

Comment garantir la sûreté des pointeurs dans l’arena

  • Lorsqu’on n’utilise que des pointeurs alloués à l’intérieur de l’arena, le GC conserve l’intégralité de l’arena
  • En faisant référencer l’arena par les pointeurs, on garantit la survie de toute l’arena vis-à-vis du GC
  • La méthode d’allocation de l’arena effectue notamment ceci :
    • dans allocChunk(), elle stocke le pointeur de l’arena à la fin du chunk

Résultats des benchmarks de performance

  • Par rapport à new de base, l’allocation par arena montre en moyenne des performances 2 à 4 fois supérieures, voire davantage
  • Même dans des situations de forte charge GC, l’approche par arena montre des performances supérieures pouvant dépasser un facteur 2
  • Des optimisations comme la suppression des write barriers ou l’usage de uintptr apportent jusqu’à 20 % de gain sur les petites allocations

Réutilisation des chunks et stratégie de suppression du heap

  • Il est possible de réutiliser les chunks avec sync.Pool
  • runtime.SetFinalizer() permet de renvoyer les chunks dans le pool quand l’arena disparaît
  • Les performances s’améliorent nettement sur les petites allocations, mais peuvent devenir inférieures à new sur les grosses allocations

Réinitialisation et réutilisation de l’arena

  • La méthode Reset() permet de ramener l’arena à son état initial
  • C’est potentiellement risqué, mais cela permet de réutiliser la même structure sans réallouer la mémoire
  • La réutilisation des chunks lors de cette remise à zéro améliore fortement les performances

Implémentation de la fonctionnalité Realloc

  • L’arena implémente une fonctionnalité de realloc permettant une extension dynamique de l’allocation la plus récente
  • Quand ce n’est pas possible, une nouvelle zone mémoire est allouée puis les données sont copiées

Conclusion et code complet fourni

  • En comprenant en profondeur le mécanisme de GC de Go et en s’appuyant sur son implémentation interne, l’auteur aboutit à un gestionnaire mémoire basé sur une arena
  • La structure combine sûreté et performances, et peut être très utile pour traiter de grandes structures de données lorsqu’elle est utilisée à bon escient
  • Le code complet inclut la structure Arena ainsi que New, Alloc, Reset, allocChunk, finalize, etc.

1 commentaires

 
GN⁺ 2025-04-24
Commentaire Hacker News
  • Cet article est une lecture intéressante

    • Si vous avez apprécié cet article ou si vous voulez mieux contrôler l’allocation mémoire en Go, je vous invite à jeter un œil au package que j’ai écrit
    • Je serais ravi d’avoir des retours ou que d’autres personnes l’utilisent
    • Ce package alloue sa propre mémoire séparément du runtime, ce qui contourne complètement le GC
    • Il n’accepte pas les types pointeurs lors de l’allocation, mais les remplace par un type Reference[T] offrant la même fonctionnalité
    • La libération de la mémoire se fait manuellement, on ne peut donc pas s’en remettre au garbage collector
    • En Go, ce type d’allocateur personnalisé vise généralement des arènes qui prennent en charge des groupes d’allocations créés et détruits ensemble
    • Cependant, le package offheap vise à construire de grandes structures de données de longue durée avec un coût de garbage collection nul
    • Par exemple de grands caches en mémoire ou des bases de données
  • En faisant récemment du tuning de performance en Go, j’ai fini par utiliser un design d’arène très similaire pour maximiser les performances

    • J’utilise des slices d’octets comme buffer et comme chunks au lieu de pointeurs unsafe
    • J’ai essayé ainsi, mais ce n’était pas plus rapide et c’était bien plus complexe
    • Je dois revérifier avant d’en être sûr à 100 %
  • Quelques améliorations simples

    • Si vous commencez avec une petite slice et qu’une partie de la payload est ajoutée en masse, écrivez votre propre append qui augmente le cap de façon plus agressive avant d’appeler l’append intégré
    • unsafe.String est utile pour passer une chaîne depuis une slice d’octets sans allocation
    • Il faut lire attentivement les avertissements et bien comprendre ce que l’on fait
  • Hors sujet, mais j’aime bien la mini-carte sur le côté

    • Elle est utile dans les longs articles techniques quand on navigue dans le contenu ou qu’on revient à quelque chose qu’on a déjà lu
    • Je me demande comment je pourrais l’ajouter à mon site
    • C’est vraiment chouette
  • Résumé pour ceux qui hésitent à lire un long article

    • L’OP construit un allocateur d’arène en Go avec unsafe pour accélérer le travail d’allocation
    • C’est particulièrement utile quand on alloue beaucoup d’objets créés et détruits ensemble
    • Le principal problème est que le GC de Go doit connaître le layout des données, en particulier l’emplacement des pointeurs, pour fonctionner correctement
    • Si l’on alloue des octets bruts avec unsafe.Pointer, le GC ne peut pas correctement voir ce qui est pointé depuis l’arène et peut le libérer par erreur
    • Cependant, pour que cela fonctionne tant que les pointeurs pointent vers d’autres éléments de la même arène, l’arène entière est conservée tant qu’une partie reste référencée
    • Il (1) conserve une slice de tous les gros blocs mémoire obtenus par l’arène auprès du système (les chunks)
    • et (2) utilise reflect.StructOf pour créer un nouveau type incluant des champs pointeurs supplémentaires vers ces blocs
    • Ainsi, si le GC trouve un pointeur vers les chunks, il trouve aussi les pointeurs de retour, marque l’arène comme vivante et conserve la slice de chunks
    • L’article présente ensuite des techniques d’optimisation intéressantes pour supprimer diverses vérifications internes et write barriers
  • Connexe : discussion sur l’ajout de « zones mémoire » à la bibliothèque standard

    • Proposition précédente sur les arènes
  • C’est intéressant

    • Je me demande généralement comment les personnes qui construisent des allocateurs off-heap ou de style arène en Go testent ou benchmarkent la sûreté mémoire et l’interaction avec le GC
  • Go donne la priorité à l’absence de rupture de l’écosystème

    • Cela permet de supposer que la loi de Hyrum protégera certains comportements observables spécifiques du runtime
    • Si cette affirmation est correcte, alors Go est, en tant que langage, dans une impasse évolutive
    • Dans ce cas, je ne suis pas certain que Go soit intéressant
  • Petite note méta

    • Cet article est vraiment très long et je n’ai pas le temps de lire autant de détails de contexte
    • Par exemple, la section « Mark and Sweep » occupe plus de 4 pages sur l’écran de mon ordinateur portable
    • Cette section ne commence qu’après plus de 5 pages d’article
    • Je me demande si c’est le résultat d’une aide de l’IA à la rédaction de sections devenues trop exhaustives
    • Générer du contenu est facile, mais les décisions éditoriales nécessaires pour ne garder que l’essentiel n’ont pas été prises
    • Je veux seulement la partie sur les allocateurs d’arène, pas un tutoriel sur le garbage collection