Bonnes pratiques pour réduire l'activité du garbage collector en Javascript

94

J'ai une application Javascript assez complexe, qui a une boucle principale appelée 60 fois par seconde. Il semble y avoir beaucoup de ramasse-miettes en cours (basé sur la sortie `` en dents de scie '' de la chronologie de la mémoire dans les outils de développement Chrome) - et cela a souvent un impact sur les performances de l'application.

J'essaie donc de rechercher les meilleures pratiques pour réduire la quantité de travail que le garbage collector doit effectuer. (La plupart des informations que j'ai pu trouver sur le Web concernent éviter les fuites de mémoire, ce qui est une question légèrement différente - ma mémoire se libère, c'est juste qu'il y a trop de ramasse-miettes en cours.) 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» sur le modèle de l'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 qui peuvent être appliquées à tous les niveaux, en particulier en ce qui concerne les fonctions qui sont appelées très souvent dans des boucles serrées. .

Quelles techniques puis-je utiliser pour réduire la quantité de travail que le garbage collector doit effectuer?

Et peut-être aussi - quelles techniques peuvent être employées pour identifier les objets qui sont le plus ramassés? (C'est une base de code très volumineuse, donc comparer des instantanés du tas n'a pas été très fructueux)

Jusqu'à la crique
la source
2
Avez-vous un exemple de votre code que vous pourriez nous montrer? Il sera alors plus facile de répondre à la question (mais aussi potentiellement moins générale, donc je ne suis pas sûr ici)
John Dvorak
2
Que diriez-vous d'arrêter d'exécuter des fonctions des milliers de fois par seconde? Est-ce vraiment la seule façon d'aborder cela? Cette question semble être un problème XY. Vous décrivez X mais ce que vous cherchez vraiment, c'est une solution à Y.
Travis J
2
@TravisJ: Il ne l'exécute que 60 fois par seconde, ce qui est un taux d'animation assez courant. Il ne demande pas de faire moins de travail, mais comment le faire de manière plus efficace pour le ramassage des ordures.
Bergi
1
@Bergi - "certaines fonctions peuvent être appelées des milliers de fois par seconde". C'est une fois par milliseconde (peut-être pire!). Ce n'est pas du tout courant. 60 fois par seconde ne devrait pas être un problème. Cette question est trop vague et ne produira que des opinions ou des suppositions.
Travis J
4
@TravisJ - Ce n'est pas du tout rare dans les frameworks de jeu.
UpTheCreek

Réponses:

127

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

L'attribution se produit dans les interprètes modernes à plusieurs endroits:

  1. Lorsque vous créez un objet via newou via la 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 contraint à un objet comme Object(myNumber)ouNumber.prototype.toString.call(42)
  7. Lorsque vous appelez un builtin qui fait l'un de ces sous le capot, comme Array.prototype.slice.
  8. Lorsque vous utilisez argumentspour réfléchir sur la liste des paramètres.
  9. Lorsque vous divisez une chaîne ou une correspondance avec une expression régulière.

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

Plus précisément, recherchez des opportunités pour:

  1. Tirez les fonctions internes qui n'ont pas ou peu de dépendances sur un état fermé dans une portée plus élevée et plus longue. (Certains minificateurs de code comme le compilateur Closure peuvent incorporer des fonctions internes et peuvent améliorer les performances de votre GC.)
  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 à plusieurs reprises l'utilisation splitou les correspondances d'expressions régulières, car chacune nécessite plusieurs allocations d'objets. Cela se produit fréquemment avec les clés dans les tables de recherche et les ID de nœud DOM dynamiques. Par exemple, lookupTable['foo-' + x]et les document.getElementById('foo-' + x)deux impliquent une allocation car il existe une concaténation de chaînes. Souvent, vous pouvez attacher des clés à des objets de longue durée au lieu de les reconcaténer. Selon les navigateurs que vous devez prendre en charge, vous pourrez peut-être utiliser Mapdirectement des objets comme clés.
  3. Évitez d'attraper 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 fonction intégrée comme JSON.stringifyqui 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, passez comme rappel une fonction de longue durée (voir 1) qui recrée l'état à partir du contenu du message.
  6. Évitez d'utiliser argumentsdepuis les fonctions qui utilisent qui doivent créer un objet de type tableau lorsqu'elles sont appelées.

J'ai suggéré d'utiliser JSON.stringifypour créer des messages réseau sortants. L'analyse des messages d'entrée JSON.parseimplique évidemment une allocation, et beaucoup pour les messages volumineux. Si vous pouvez représenter vos messages entrants sous forme de tableaux de primitives, vous pouvez économiser de nombreuses allocations. Le seul autre élément intégré autour duquel vous pouvez créer un analyseur qui n'alloue pas est String.prototype.charCodeAt. Un analyseur pour un format complexe qui n'utilise que ce qui va être infernal à lire.

Mike Samuel
la source
Ne pensez-vous pas que les JSON.parseobjets d allouent moins (ou autant) d'espace que la chaîne de message?
Bergi
@Bergi, cela dépend du fait que les noms de propriétés nécessitent des allocations séparées, mais un analyseur qui génère des événements au lieu d'un arbre d'analyse ne fait pas d'allocations superflues.
Mike Samuel
Réponse fantastique, merci! Beaucoup d'excuses pour l'expiration de la prime - je voyageais à l'époque et, pour une raison quelconque, je ne pouvais pas me connecter à SO avec mon compte gmail sur mon téléphone ....: /
UpTheCreek
Pour compenser mon mauvais timing avec la prime, j'en ai ajouté une supplémentaire pour la compléter (200 était le minimum que je pouvais donner;) - Pour une raison quelconque, même si cela m'oblige à attendre 24 heures avant de l'attribuer (même si J'ai sélectionné «récompense la réponse existante»). Sera à vous demain ...
UpTheCreek
@UpTheCreek, pas de soucis. Je suis content que vous l'ayez trouvé utile.
Mike Samuel du
13

Les outils de développement Chrome ont une fonctionnalité très intéressante pour tracer l'allocation de mémoire. Cela s'appelle la chronologie de la mémoire. Cet article décrit quelques détails. Je suppose que c'est ce dont vous parlez concernant la "dent de scie"? Il s'agit d'un comportement normal pour la plupart des environnements d'exécution GC. L'allocation se poursuit jusqu'à ce qu'un seuil d'utilisation soit atteint, déclenchant une collecte. Normalement, il existe différents types de collections à différents seuils.

Chronologie de la mémoire dans Chrome

Les récupérations de mémoire sont incluses dans la liste d'événements associée à la trace avec leur durée. Sur mon cahier assez ancien, des collections éphémères se produisent à environ 4 Mo et prennent 30 ms. Il s'agit de 2 de vos itérations de boucle à 60 Hz. S'il s'agit d'une animation, les collections de 30 ms provoquent probablement un bégaiement. Vous devriez commencer ici pour voir ce qui se passe dans votre environnement: où se situe le seuil de collecte et combien de temps vos collections prennent. 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, allongeant ainsi l'intervalle entre les collectes.

La prochaine étape consiste à utiliser les profils | Fonction Record Heap Allocations pour générer un catalogue d'allocations par type d'enregistrement. Cela montrera rapidement quels types d'objets consomment le plus de mémoire pendant la période de trace, ce qui est équivalent au taux d'allocation. Concentrez-vous sur ces derniers par ordre décroissant de taux.

Les techniques ne sont pas sorcières. Évitez les objets en boîte lorsque vous pouvez le faire avec un sans boîte. Utilisez des variables globales pour contenir et réutiliser des objets en boîte uniques plutôt que d'allouer de nouveaux objets à chaque itération. Regroupez les types d'objets communs dans des listes gratuites plutôt que de les abandonner. Mettez en cache les résultats de concaténation de chaînes qui sont probablement réutilisables dans les itérations futures. Évitez l'allocation juste pour renvoyer des résultats de fonction en définissant des variables dans une portée englobante à la place. 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, publiez une modification décrivant les détails du défi que vous envisagez.

Je déconseille de pervertir votre style de codage normal tout au long d'une application dans une tentative de fusil de chasse pour 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 ainsi qu'une grande partie de la complexité et de l'obscurité supplémentaires du code n'auront aucun sens.

Gène
la source
C'est ce que je veux dire par la dent de scie. Je sais qu'il y aura toujours un motif en dents de scie, mais ce qui me préoccupe, c'est qu'avec mon application, la fréquence des dents de scie et les «falaises» sont assez élevées. Fait intéressant, les événements GC ne se présentent pas sur ma ligne de temps - les seuls événements qui apparaissent dans le volet « dossiers » (celui du milieu) sont: request animation frame, animation frame firedet composite layers. Je ne sais pas pourquoi je ne vois pas GC Eventcomme vous (c'est sur la dernière version de chrome, et aussi canary).
UpTheCreek
4
J'ai essayé d'utiliser le profileur avec des «allocations de tas d'enregistrement», mais jusqu'à présent, je ne l'ai pas trouvé très utile. C'est peut-être parce que je ne sais pas comment l'utiliser correctement. Il semble plein de références qui ne me disent rien, comme @342342et code relocation info.
UpTheCreek
9

En règle générale, vous voudriez mettre en cache autant que possible et faire aussi peu de création et de destruction 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. De plus, il serait facile de tomber dans le piège de la création et de la destruction d'objets qui sont passés à d'autres fonctions. Je ne suis en aucun cas un expert 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);
}

fonctionnerait beaucoup plus vite 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 que vous en avez besoin pour fonctionner correctement pendant une seconde ou deux (par exemple pour une animation) et ensuite il a plus de temps à traiter? Si tel est le cas, je pourrais voir prendre des objets qui seraient normalement récupérés tout au long de l'animation et garder une référence à eux dans un objet global. Ensuite, lorsque l'animation se termine, vous pouvez effacer toutes les références et laisser le garbage collector faire son travail.

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

Chris B
la source
Ce. De plus, les fonctions mentionnées dans d'autres fonctions (qui ne sont pas des IIFE) sont également des abus courants qui brûlent beaucoup de mémoire et sont faciles à manquer.
Esailija
Merci Chris! Je n'ai malheureusement pas de temps d'arrêt: /
UpTheCreek
4

Je créerais un ou quelques objets dans le global scope(où je suis sûr que le garbage collector n'est pas autorisé à les toucher), puis j'essaierais de refactoriser ma solution pour utiliser ces objets pour faire le travail, au lieu d'utiliser des variables locales .

Bien sûr, cela ne peut pas être fait partout dans le code, mais en général, c'est ma façon d'éviter le garbage collector.

PS Cela pourrait rendre cette partie spécifique du code un peu moins maintenable.

Mahdi
la source
Le GC prend mes variables de portée globale de manière cohérente.
VectorVortec