Allocateur de mémoire personnalisé
LibSass est livré avec un allocateur de mémoire personnalisé pour améliorer les performances. Initialement inclus dans LibSass 3.6, il est actuellement désactivé par défaut. Son activation est requise via la définition de SASS_CUSTOM_ALLOCATOR.
Présentation
L'allocateur est un allocateur de pool/arène surmonté d'une liste libre. Le modèle d'utilisation de la mémoire de LibSass est parfaitement adapté à cette implémentation. Chaque compilation tend à accumuler de la mémoire et à ne libérer que certains éléments de temps à autre, mais la consommation globale de mémoire augmente généralement jusqu'à la fin de la compilation. Cela nous permet de simplifier l'implémentation au maximum, car nous n'avons pas besoin de libérer beaucoup de mémoire au système et pouvons la réutiliser.
Arènes
Chaque arène est allouée selon une taille fixe (à la compilation). Chaque requête d'allocation est traitée depuis l'arène actuelle. Ils découpe l'arène en blocs de différentes tailles. Les arènes ne sont jamais restituées au système tant que la compilation n'est pas terminée.
Tranches
Une tranche de mémoire est une partie d'une arène. Lorsque le système demande un bloc de mémoire dimensionné, ils vérifient si l'arène actuelle dispose de suffisamment d'espace pour le contenir. Dans le cas contraire, une nouvelle arène est allouée. Nous renvoyons ensuite un pointeur vers cette arène et marquons l'espace comme utilisé. Chaque tranche possède également un en-tête invisible pour le demandeur, car il précède l'adresse du pointeur renvoyée. Cet entête est utilisé à des fins de comptabilité.
Listes libres (ou compartiments)
Une fois qu'une tranche de mémoire est renvoyée à l'allocateur, elle n'est pas libérée. Elle est placée sur la liste libre. Ils conservent un nombre fixe de listes libres, une pour chaque taille de bloc possible. Comme les tailles de bloc sont alignées sur la mémoire, nous pouvons obtenir l'index de la liste libre (ou compartiment) très rapidement (taille/alignement).
Tailles de bloc
Les arènes étant de taille fixe, ils doivent s'assurer que seuls des blocs suffisamment petits en sont servis. Cela simplifie également l'implémentation, car ils pouvent déclarer statiquement certaines structures pour la comptabilité. Les allocations trop importantes pour être suivies dans une liste libre seront directement affectées à malloc et free. C'est le cas lorsque l'index du compartiment est supérieur à SassAllocatorBuckets.
Sécurité des processus légers
Cette implémentation n'est pas thread-safe par conception. La rendre thread-safe serait certainement possible, mais cela aurait un impact (en termes de performances). De plus, cela n'est pas nécessaire compte tenu du modèle d'utilisation de la mémoire de LibSass. Il est donc préférable de s'assurer que les bassins de mémoire sont locaux pour chaque processus léger.
Obstacles à l'implémentation
L'allocation de mémoire étant un élément essentiel du C++, nous nous trouvons confrontés à plusieurs difficultés. Cela s'est particulièrement avéré pour l'initialisation et l'ordre de destruction des variables statiques. Par exemple, lorsqu'une chaîne statique possède un allocateur personnalisé, il se peut qu'elle soit initialisée avant le bassin de mémoire local du processus léger. D'un autre côté, il est également possible que le bassin de mémoire soit détruit avant qu'une autre chaîne statique ne veuille lui restituer sa mémoire. J'ai essayé de contourner ces problèmes, principalement en utilisant uniquement des objets POD (plain old data) locaux.
Gains de performances
Les tests indiquent que l'allocateur personnalisé améliore les performances d'environ 15 % pour les cas complexes (ils ont utilisé Bolt-Bench pour le mesurer). Une fois optimisé, l'allocateur personnalisé peut apporter jusqu'à 30 % d'amélioration. Cela représente une faible perte de quelques pourcents d'utilisation mémoire globale. Des ajustements sont possibles, mais le point idéal pour l'instant semble être :
- // Combien de compartiments devrait-ils avoir pour la liste libre ?
- // Détermine quand les allocations vont directement à malloc/free
- // Pour obtenir la taille maximale des éléments gérés, multipliez par l'alignement.
- #define SassAllocatorBuckets 512
-
- // La taille des arènes du bassin de mémoire en octets.
- #define SassAllocatorArenaSize (1024 * 256)
Ces informations se trouvent dans settings.hpp.
Surcharge mémoire
Les paramètres SassAllocatorBuckets et SassAllocatorArenaSize doivent être définis l'un par rapport à l'autre. En supposant que l'alignement mémoire sur la plateforme soit de 8 octets, la taille maximale des blocs pouvant être gérés est de 4 Ko (512 x 8 octets). Si la taille de l'arène est trop proche de cette valeur, vous risquez de laisser beaucoup de RAM inutilisée. Lorsqu'une arène ne peut pas répondre à la requête en cours, elle est mise de côté et une nouvelle est allouée. Nous ne gardons pas trace de l'espace inutilisé des arènes précédentes, car cela surcharge le code et consomme un temps processeur précieux. En définissant les valeurs avec soin, nous pouvons éviter ce coût tout en conservant une surcharge mémoire raisonnable. Dans le pire des cas, la perte est d'environ 1,5 % par rapport aux paramètres par défaut (4 Ko sur 256 Ko).
Améliorations supplémentaires
Il serait intéressant de vérifier s'il est possible de réutiliser l'espace des anciennes arènes sans trop impacter les performances. Ils peuvent également vérifier les listes libres de blocs de plus grande taille pour répondre à une demande d'allocation. Cependant, ces deux éléments devraient être vérifiés pour évaluer l'impact sur les performances et leur gain réel.