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)
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:
new
ou via une syntaxe littérale [...]
Ou {}
.(function (...) { ... })
.Object(myNumber)
ou Number.prototype.toString.call(42)
Array.prototype.slice
.arguments
pour réfléchir sur la liste de paramètres.Évitez de faire cela, et regroupez et réutilisez des objets lorsque cela est possible.
Plus précisément, recherchez les occasions 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.try { op(x) } catch (e) { ... }
, faites if (!opCouldFailOn(x)) { op(x); } else { ... }
.JSON.stringify
, qui utilise un tampon natif interne pour accumuler du contenu au lieu d'allouer plusieurs objets.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.
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.
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.
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é.
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.