Static Memory Javascript with Object Pools

HTML5 Rocks

Introduction

Vous recevez un e-mail disant que votre jeu/application web se comporte mal au bout d'un certain temps, vous cherchez dans votre code, ne remarquez rien de particulier, jusqu'à ce que vous ouvriez les outils de performance mémoire de Chrome, et que vous voyiez ça :

Je me demande ce que sont ces dents de scie ?

Un de vos collègues glousse parce qu'il comprend que vous avez un problème de performance lié à la gestion de la mémoire.

Dans le graphique représentant la mémoire, ce motif en dents de scie est très représentatif d'un potentiel problème critique de performance. Tant que votre utilisation mémoire grandit, la zone du graphique grossit aussi dans le temps. Lorsque la courbe plonge soudainement, c'est que le ramasse-miettes est passé et a nettoyé vos objets référencés en mémoire.

Jetez un oeil à tous ces événements du ramasse-miettes !

Dans un graphique comme ça, vous pouvez voir qu'il y a beaucoup d'événements dus au ramasse-miettes, ce qui peut être nuisible aux performance de votre application web. Cet article vous montrera comment reprendre le contrôle sur votre utilisation mémoire et réduire l'impact sur les performances.

Ramasse-miettes et coûts en performance

Le modèle mémoire de JavaScript est basé sur une technologie appelée ramasse-miettes. Dans beaucoup de langages, le développeur est directement responsable d'allouer et de libérer la mémoire à partir du tas mémoire du système. Cependant un système de ramasse-miettes peut s'occuper de cette tâche pour le développeur, ce qui signifie que les objets ne sont pas directement libérés de la mémoire lorsque le développeur les déréférence, mais plus tard quand les heuristiques du ramasse-miettes décident que ce serait bien de le faire. Ce processus de décision requiert que le ramasse-miettes fasse quelques analyses statistiques sur les objets actifs et inactifs, ce qui peut prendre un certain temps.

En informatique, un ramasse-miettes (GC, garbage collector) est une forme de gestion automatique de la mémoire. Le ramasse-miettes tente de récupérer les déchets, ou la mémoire occupée par des objets qui ne sont plus utilisés par le programme.

Le ramassage de miettes est souvent décrit comme l'opposé de la gestion manuelle de la mémoire, laquelle demande au développeur de spécifier quels sont les objets à désallouer et à retourner à la mémoire du système.

Le processus par lequel un ramasse-miettes récupère la mémoire n'est pas gratuit, il empiète sur les performances possibles en prenant un certain temps pour faire son travail ; en plus de ça, le système décide lui-même quand il s'exécute. Vous n'avez aucun contrôle là-dessus, une action du ramasse-miettes peut survenir n'importe quand pendant l'exécution du code, ce qui bloque l'exécution du code jusqu'à ce que le nettoyage soit terminé. La durée de cette action vous est généralement inconnue ; cela prendra un certain temps à s'exécuter, en fonction de comment votre programme utilise la mémoire à un moment donné.

Les applications hautes performances s'appuie sur des limites de performance cohérentes pour assurer une expérience agréable aux utilisateurs. Les systèmes de ramasse-miettes peuvent court-circuiter ce but, car ils s'exécutent à des moments aléatoires pour une durée aléatoire, consommant ainsi le temps disponible dont l'application a besoin pour respecter ses objectifs de performance.

Réduire le churn mémoire, réduire les taxes du ramasse-miettes

Comme vu précédemment, une action du ramasse-miettes survient une fois qu'un ensemble d'heuristiques a déterminé qu'il y a assez d'objets inactifs pour que ce soit bénéfique. Ainsi, la clé pour réduire le temps que le ramasse-miettes prend à votre application repose sur l'élimination des cas de création/libération excessive d'objets. Ce processus de création/libération fréquente d'objets est appelé "churn mémoire" (change and turn, attrition). Si vous pouvez réduire le churn mémoire durant le cycle de vie de votre application, vous réduisez aussi le temps que le ramasse-miettes prend à l'exécution de votre application. Cela signifie que vous avez besoin de supprimer/réduire le nombre d'objets créés et détruits ; effectivement, vous devez arrêter d'allouer de la mémoire. Ce processus transformera votre mémoire de ça :

Je me demande ce que sont ces dents de scie ?

en ça :

Ahhhh, c'est mieux.

Dans ce modèle, vous pouvez voir que la courbe n'a plus de motif en dents de scie, mais grandit plutôt une bonne fois au début, et ensuite augmente légèrement au fil du temps. Si vous rencontrez des problèmes de performance à cause du churn mémoire, c'est ce type de courbe que vous voulez créer.

Vers la mémoire statique en JavaScript

La mémoire statique en JavaScript est une technique qui implique de pré-allouer, au démarrage de votre application, toute la mémoire qui sera nécessaire pour sa durée de vie, et de gérer cette mémoire durant l'exécution dès que les objets ne sont plus utiles. Nous pouvons approcher cet objectif en quelques étapes :

  1. Instrumentaliser votre application pour déterminer quel est le nombre maximum d'objets mémoire requis (par type) pour un certain nombre de cas d'utilisation ;
  2. Ré-implémenter votre code pour pré-allouer la quantité maximale d'objets, et ensuite les récupérer/libérer manuellement plutôt que d'aller les chercher dans la mémoire principale.
En réalité, accomplir #1 nous demande de faire un peu de #2, donc commençons par là.

Pool d'objets

Simplement, mettre des objets dans un pool consiste à conserver un ensemble d'objets inutilisés qui partagent le même type. Quand vous avez besoin d'un nouvel objet pour votre code, plutôt que d'en allouer un nouveau à partir du tas mémoire du système, vous en recyclez un inutilisé à partir du pool. Une fois que le code externe en a fini avec cet objet, plutôt que de le libérer dans la mémoire principale, il est remis dans le pool. Parce que l'objet n'est jamais déréférencé (ou effacé) du code il ne sera pas récupéré par le ramasse-miettes. Utiliser des pools d'objets met le contrôle de la mémoire dans les mains du développeur, réduisant l'influence du ramasse-miettes sur les performances.

Les pools d'objets sont une pratique commune dans beaucoup d'applications hautes performances, car ils réduisent la quantité d'aller-retours de la mémoire avec le système. Les pools d'objets eux-mêmes ont deux caractéristiques principales :
  1. Un pool verra son empreinte mémoire grandir avec le nombre d'objets qu'il contient.
  2. Le nombre d'objets créés/libérés chutera au minimum requis par votre application.

Puisqu'une application maintient un ensemble hétérogène de types d'objet, un usage propre des pools d'objets demande que vous ayez un pool par type qui connaît une haut churn durant l'exécution de votre application.

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... effectuer quelque chose dont nous avons besoin avec l'objet

gEntityObjectPool.free(newEntity); //libérer l'objet lorsque nous avons fini
newEntity = null; //libérer la référence de l'objet

Pour la large majorité des applications, vous atteindrez éventuellement une stabilisation en termes de besoin d'allouer de nouveaux objets. Après plusieurs exécutions de votre application, vous devriez être capable d'avoir un bonne idée de la limite haute, et ainsi pouvoir pré-allouer le bon nombre d'objets au démarrage de votre application.

Pré-allouer des objets

Implémenter un pool d'objets dans votre projet vous donnera un supérieur théorique pour le nombre d'objets requis durant l'exécution de votre application. Après avoir testé votre site avec plusieurs scénarii, vous pouvez avoir une bonne idée des types de besoins en mémoire qui seront nécessaires, et pouvez cataloguer les données quelque part, et les analyser pour comprendre quelles sont les limites supérieures requises pour votre application.

Ensuite, dans la version de livraison de votre application, vous pouvez paramétrer la phase d'initialisation pour pré-remplir les pools d'objets avec une quantité spécifiée. Ceci va forcer toute l'initialisation des objets au démarrage de votre application, et réduire le nombre d'allocations qui se produisent de façon dynamique lors de son exécution.

function init() {
  //pré-alloue tous nos pools. 
  //Notez que nous gardons chaque pool homogène par type d'objets
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

La quantité que vous choisissez a un fort lien avec le comportement de votre application ; parfois le maximum théorique n'est pas la meilleure option. Par exemple, choisir le niveau supérieur moyen peut vous donner une empreinte mémoire plus petite pour les utilisateurs non-avancés.

Loin d'être une solution miracle

Il y a tout une classification d'application pour lesquelles les motifs de croissance de la mémoire statique peut être un gain. Cependant, comme le camarade Renato Mangini de Chrome DevRel l'a signalé, il y a quelques inconvénients.

Les pools ne sont pas pour tout le monde, même pour des applications hautes performances. Considérez les compromis suivants avant d'adopter les pools d'objets et les méthodes de mémoire statique : le temps de démarrage sera plus long, puisque vous dépensez des cycles à allouer la mémoire lors de l'initialisation. La mémoire ne diminue pas dans les scénarii qui utilisent peu d'objets ; votre application consommera avidement la mémoire. Vous aurez parfois besoin de nettoyer un objet quand il sera retourné au pool, cela peut être une surcharge non négligeable durant les périodes de haut churn mémoire.

Conclusion

Une des raisons qui fait que JavaScript est idéal pour le web est qu'il est rapide, sympa et facile pour débuter. Ceci est principalement dû à une barrière basse en termes de restrictions syntaxiques et qu'il gère les problèmes de mémoire pour vous. Vous pouvez coder et lui laisser faire le sale boulot. Cependant pour des applications web hautes performances, comme les jeux HTML5, le ramasse-miettes peut souvent altérer sévèrement le nombre d'images par seconde, dégradant ainsi l'expérience de l'utilisateur final. Avec quelques prudentes instrumentations et l'adoption des pools d'objets, vous pouvez réduire cette charge sur votre taux de d'images, et récupérer ce temps pour des choses plus impressionnantes.

Code source

Il y a beaucoup d'implémentations de pools d'objets partout sur le web, donc je ne vais pas vous ennuyer avec une autre. À la place, je vais vous rediriger vers les existantes, chacune ayant des nuances de mise en oeuvre ; ce qui est important, étant donné que chaque usage peut avoir des besoins spécifiques.

Références

Comments

0