web-dev-qa-db-fra.com

Meilleures pratiques pour réduire l'activité de Garbage Collector en Javascript

J'ai une application Javascript assez complexe, qui a une boucle principale appelée 60 fois par seconde. Il semble y avoir beaucoup de récupération de place en cours (basée sur la sortie en dents de scie de la timeline de la mémoire dans = Chrome dev)), ce qui a souvent une incidence sur les performances de l'application.

J'essaie donc de rechercher les meilleures pratiques pour réduire la quantité de travail que le ramasse-miettes doit faire. (La plupart des informations que j'ai pu trouver sur le Web concernent la prévention des fuites de mémoire. C'est une question légèrement différente. Ma mémoire est libérée, c'est simplement qu'il y a trop de nettoyage des ordures.) Je suppose que cela revient principalement à réutiliser des objets autant que possible, mais bien sûr, le diable est dans les détails.

L'application est structurée en 'classes' dans le sens de héritage JavaScript simple de John Resig .

Je pense qu'un problème est que certaines fonctions peuvent être appelées des milliers de fois par seconde (car elles sont utilisées des centaines de fois à chaque itération de la boucle principale), et peut-être les variables de travail locales dans ces fonctions (chaînes, tableaux, etc.) pourrait être le problème.

Je suis conscient de la mise en commun d'objets pour des objets plus gros/plus lourds (et nous l'utilisons dans une certaine mesure), mais je recherche des techniques pouvant être appliquées à tous les niveaux, en particulier en ce qui concerne les fonctions appelées très souvent dans des boucles serrées. .

Quelles techniques puis-je utiliser pour réduire la quantité de travail que le ramasse-miettes doit faire?

Et peut-être aussi - quelles techniques peut-on utiliser pour identifier quels objets sont le plus collectés? (Il s'agit d'un code de base très volumineux, comparer des instantanés du tas n'a donc pas été très fructueux)

89
UpTheCreek

Beaucoup de choses que vous devez faire pour minimiser le taux de désabonnement du GC vont à l'encontre de ce qui est considéré comme un JS idiomatique dans la plupart des autres scénarios. Par conséquent, veuillez garder à l'esprit le contexte lorsque vous jugez les conseils que je donne.

La répartition se fait chez les interprètes modernes à plusieurs endroits:

  1. Lorsque vous créez un objet via new ou via une syntaxe littérale [...] Ou {}.
  2. Lorsque vous concaténez des chaînes.
  3. Lorsque vous entrez une étendue contenant des déclarations de fonction.
  4. Lorsque vous effectuez une action qui déclenche une exception.
  5. Lorsque vous évaluez une expression de fonction: (function (...) { ... }).
  6. Lorsque vous effectuez une opération qui oblige Object comme Object(myNumber) ou Number.prototype.toString.call(42)
  7. Lorsque vous appelez un programme intégré qui effectue l'une de ces tâches sous le capot, comme Array.prototype.slice.
  8. Lorsque vous utilisez arguments pour réfléchir sur la liste de paramètres.
  9. Lorsque vous divisez une chaîne ou que vous faites correspondre une expression régulière.

Évitez de faire cela, et regroupez et réutilisez des objets lorsque cela est possible.

Plus précisément, recherchez les occasions de:

  1. Tirez les fonctions internes qui ont peu ou pas de dépendances sur l'état fermé dans une portée plus longue et plus durable. (Certains minificateurs de code tels que compilateur Closure peuvent intégrer des fonctions internes et pourraient améliorer les performances de votre CPG.)
  2. Évitez d'utiliser des chaînes pour représenter des données structurées ou pour un adressage dynamique. Évitez en particulier d'analyser de manière répétée à l'aide de split ou de correspondances d'expressions régulières, car chacune nécessite plusieurs allocations d'objet. Cela se produit souvent avec des clés dans des tables de recherche et des ID de nœud DOM dynamiques. Par exemple, lookupTable['foo-' + x] Et document.getElementById('foo-' + x) impliquent tous deux une allocation puisqu'il existe une concaténation de chaîne. Vous pouvez souvent associer des clés à des objets de longue durée au lieu de les concaténer à nouveau. Selon les navigateurs à prendre en charge, vous pourrez peut-être utiliser Map pour utiliser directement des objets comme clés.
  3. Évitez de capturer des exceptions sur les chemins de code normaux. Au lieu de try { op(x) } catch (e) { ... }, faites if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Lorsque vous ne pouvez pas éviter de créer des chaînes, par exemple pour passer un message à un serveur, utilisez une commande intégrée telle que JSON.stringify, qui utilise un tampon natif interne pour accumuler du contenu au lieu d'allouer plusieurs objets.
  5. Évitez d'utiliser des rappels pour les événements à haute fréquence et, lorsque vous le pouvez, transmettez en tant que rappel une fonction de longue durée (voir 1) qui recrée l'état du contenu du message.
  6. Évitez d’utiliser arguments car les fonctions qui en utilisent doivent créer un objet de type tableau lorsqu’elles sont appelées.

J'ai suggéré d'utiliser JSON.stringify Pour créer des messages réseau sortants. L'analyse des messages d'entrée à l'aide de JSON.parse Implique évidemment une allocation, et une grande partie de celle-ci pour les messages volumineux. Si vous pouvez représenter vos messages entrants sous forme de tableaux de primitives, vous pouvez économiser beaucoup d'allocations. Le seul autre élément intégré autour duquel vous pouvez construire un analyseur qui n’alloue pas est String.prototype.charCodeAt. Un analyseur syntaxique pour un format complexe qui utilise seulement ce qui va être un enfer à lire bien.

122
Mike Samuel

Les outils de développement Chrome ont une fonctionnalité très agréable pour suivre l’allocation de mémoire. Elle s’appelle le timeline de la mémoire. Cet article décrit quelques détails. Je suppose que c’est ce que vous Il s’agit d’un comportement normal pour la plupart des environnements d’exécution de type GC. L’allocation se poursuit jusqu’à ce qu’un seuil d’utilisation soit atteint, déclenchant une collection. Normalement, il existe différents types de collections à des seuils différents.

Memory Timeline in Chrome

Les corbeilles sont incluses dans la liste des événements associés à la trace, ainsi que leur durée. Sur mon carnet de notes plutôt ancien, des collections éphémères ont lieu à environ 4 Mo et prennent 30 ms. Ceci est 2 de vos itérations de boucle 60Hz. S'il s'agit d'une animation, les collections de 30 ms provoquent probablement un bégaiement. Commencez ici pour voir ce qui se passe dans votre environnement: où se situe le seuil de collecte et combien de temps prend-il vos collections. Cela vous donne un point de référence pour évaluer les optimisations. Mais vous ne ferez probablement pas mieux que de diminuer la fréquence du bégaiement en ralentissant le taux d’allocation, en allongeant l’intervalle entre les collectes.

L'étape suivante consiste à utiliser les profils | Record Allocation Allocations pour générer un catalogue d'allocations par type d'enregistrement. Cela montrera rapidement quels types d'objet consomment le plus de mémoire au cours de la période de trace, ce qui équivaut au taux d'allocation. Concentrez-vous sur ceux-ci par ordre décroissant de taux.

Les techniques ne sont pas sournoises. Évitez les objets en boîte lorsque vous pouvez faire avec un sans boîte. Utilisez des variables globales pour conserver et réutiliser des objets à une seule boîte plutôt que d'en allouer de nouveaux à chaque itération. Regroupez les types d'objets communs dans des listes libres plutôt que de les abandonner. Résultats de concaténation de chaînes de cache susceptibles d'être réutilisés lors d'itérations futures. Évitez toute allocation pour simplement renvoyer des résultats de fonction en définissant des variables dans une portée englobante. Vous devrez considérer chaque type d'objet dans son propre contexte pour trouver la meilleure stratégie. Si vous avez besoin d’aide pour des détails spécifiques, envoyez une modification décrivant en détail le défi que vous êtes en train de relever.

Je déconseille de pervertir votre style de codage normal tout au long d'une application dans le cadre d'une tentative visant à produire moins de déchets. C’est pour la même raison que vous ne devez pas optimiser la vitesse prématurément. La plupart de vos efforts, en plus de la complexité et de l'obscurité supplémentaires du code, n'auront aucun sens.

12
Gene

En règle générale, vous voudriez mettre en cache autant que possible et créer et détruire le moins possible pour chaque exécution de votre boucle.

La première chose qui me vient à l’esprit est de réduire l’utilisation de fonctions anonymes (si vous en avez) dans votre boucle principale. En outre, il serait facile de tomber dans le piège de la création et de la destruction d'objets transférés dans d'autres fonctions. Je ne suis en aucun cas un expert en javascript, mais j'imagine que ceci:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

serait beaucoup plus rapide que cela:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

Y a-t-il des temps d'arrêt pour votre programme? Peut-être avez-vous besoin que tout se passe bien pendant une seconde ou deux (par exemple, pour une animation) et que le temps soit plus long à traiter? Si tel est le cas, je pourrais prendre des objets qui seraient normalement des ordures collectées tout au long de l'animation et conserver une référence à ces objets dans un objet global. Ensuite, lorsque l'animation se termine, vous pouvez effacer toutes les références et laisser le ramasse-miettes faire son travail.

Désolé si tout cela est un peu trivial par rapport à ce que vous avez déjà essayé et pensé.

9
Chris B

Je ferais un ou quelques objets dans le global scope (où je suis sûr que Garbage Collector n’est pas autorisé à les toucher), j’essaierais alors de refactoriser ma solution pour utiliser ces objets pour effectuer le travail au lieu d’utiliser des variables locales.

Bien sûr, cela ne pourrait pas être fait partout dans le code, mais généralement c'est ma façon d'éviter le ramasse-miettes.

P.S. Cela pourrait rendre cette partie du code un peu moins facile à gérer.

4
Mahdi