La construction d'une promesse de la chaîne de manière récursive en javascript - considérations relatives à la mémoire
Dans cette réponse, une promesse de la chaîne est construite de manière récursive.
Légèrement simplifié, nous avons :
function foo() {
function doo() {
//always return a promise
if (/* more to do */) {
return doSomethingAsync().then(doo);
} else {
return Promise.resolve();
}
}
return doo(); //returns a promise
}
Sans doute cela donnerait lieu à une pile d'appel et une promesse de la chaîne d' - ie "profonde" et "large".
Je m'attends à une mémoire de spike plus grand que l'autre l'exécution d'une récursivité ou la construction d'une promesse de la chaîne d'seule.
- Est-ce donc?
- Quelqu'un a examiné les problèmes de mémoire de la construction d'une chaîne de cette façon?
- La mémoire de consommation diffèrent entre les promesses libs?
Pouvez-vous exécuter sur JSPerf? Pourquoi suppose que lorsque vous pouvez le tester?
"on peut supposer que cela donnerait lieu à une pile d'appel" non, car il est asynchrone, la pile d'appel n'développer hors de contrôle.
Je ne suis pas sûr de savoir pourquoi cela a été downvoted, c'est une excellente question.
vous de retour d'une fonction, et non le résultat de l'appel d'une fonction, il n'est donc pas "vraiment" récursive dans une charge de travail d'estimation de sens. seulement si le retour a dû attendre sur l'exécution serait-il obtenir empilés...
Je vais attendre. Benjamin est en effet rapide, je veux dire de 6 minutes pour que la réponse est juste vulgaire 🙂
"on peut supposer que cela donnerait lieu à une pile d'appel" non, car il est asynchrone, la pile d'appel n'développer hors de contrôle.
Je ne suis pas sûr de savoir pourquoi cela a été downvoted, c'est une excellente question.
vous de retour d'une fonction, et non le résultat de l'appel d'une fonction, il n'est donc pas "vraiment" récursive dans une charge de travail d'estimation de sens. seulement si le retour a dû attendre sur l'exécution serait-il obtenir empilés...
Je vais attendre. Benjamin est en effet rapide, je veux dire de 6 minutes pour que la réponse est juste vulgaire 🙂
OriginalL'auteur Roamer-1888 | 2015-04-28
Vous devez vous connecter pour publier un commentaire.
En fait, non. Il n'y a pas de promesse de la chaîne d'ici comme nous le savons de
doSomeThingAsynchronous.then(doSomethingAsynchronous).then(doSomethingAsynchronous).…
(qui est ce quePromise.each
ouPromise.reduce
pourrait le faire de façon séquentielle exécuter des gestionnaires s'il a été écrit de cette façon).Ce que nous sommes confrontés ici est un résoudre chaîne1 - ce qui se passe à la fin, quand le cas de base de la récurrence est rencontré, c'est quelque chose comme
Promise.resolve(Promise.resolve(Promise.resolve(…)))
. Ce n'est "profond", pas "large", si vous voulez appeler ça comme ça.Pas un pic en fait. Vous auriez lentement, au fil du temps, de construire un ensemble de promesses qui sont résolus avec le plus profond, le tout représentant le même résultat. Lorsque, à la fin de votre tâche, la condition est remplie et l'intime promesse résolu avec une valeur réelle, toutes ces promesses doivent être résolus avec la même valeur. Que serait
O(n)
coût pour la marche jusqu'à la résolution de la chaîne (si mis en œuvre naïvement, ce pourrait même être fait de manière récursive et de provoquer un débordement de pile). Après cela, toutes les promesses, sauf pour les rup peuvent devenir des ordures.En revanche, une promesse de la chaîne qui est construit par quelque chose comme
montrent un pic, l'allocation de
n
promesse objets en même temps, et puis, lentement, les résoudre un par un, les ordures, collecte de la précédente, jusqu'à ce que seulement réglées à la fin de la promesse est vivant.Pas nécessairement. Comme dit ci-dessus, toutes les promesses que la plupart sont en fin de compte réglé avec la même valeur2, de sorte que tous nous avons besoin est de stocker de l'infiniment grand et l'infiniment petit de la promesse à la fois. Tous les intermédiaires de promesses peuvent devenir des ordures collectées dès que possible, et nous voulons exécuter cette récursivité dans la constante de l'espace et du temps.
En fait, cette récursive de construire est totalement nécessaire pour boucles asynchrones avec un état dynamique (pas de nombre fixe d'étapes), vous ne pouvez pas vraiment l'éviter. En Haskell, où il est utilisé tout le temps pour le
IO
monade, une optimisation pour il est mis en place seulement en raison de ce cas. Il est très similaire à queue appelons récursivité, qui est habituellement éliminés par les compilateurs.Oui. C'était discuté lors de promesses/aplus par exemple, mais sans résultat encore.
Beaucoup de promesse de bibliothèques à prendre en charge l'itération des aides pour éviter le pic de la promesse
then
les chaînes, comme le Bluebird esteach
etmap
méthodes.Ma propre promesse de la bibliothèque3,4 ne disposent d'résoudre les chaînes sans introduire de la mémoire ou de l'exécution de frais généraux. Lorsqu'une promesse adopte un autre (même s'il est toujours en attente), ils deviennent indiscernables, les intermédiaires et les promesses ne sont plus référencés.
Oui. Bien que ce cas peut être optimisé, il est rarement. Plus précisément, l'ES6 spec ne nécessitent Promet d'inspecter la valeur à chaque
resolve
appel, de sorte que l'effondrement de la chaîne n'est pas possible. Les promesses de la chaîne, peut même être résolu avec des valeurs différentes (par la construction d'un exemple d'objet d'abus de getters, pas dans la vraie vie). La question a été soulevée sur esdiscuss mais reste en suspens.Donc, si vous utilisez une fuite de la mise en œuvre, mais il faut asynchrone récursivité, alors il vaut mieux revenir à des rappels et l'utilisation de la différés antipattern à se propager le plus profond promesse résultat à un résultat unique promesse.
[1]: pas de terminologie officielle
[2]: eh bien, ils sont résolus les uns avec les autres. Mais nous voulez à régler avec la même valeur, nous attendre qui
[3]: sans-papiers, aire de jeux, passe aplus. Lire le code à vos risques et périls: https://github.com/bergus/F-Promise
[4]: également mis en œuvre pour la Croyance en ce pull request
Comme un point de données de l'allocation d'une promesse de bluebird prend moins de mémoire que l'allocation d'un tableau vide, donc si vous pensez que l'allocation de 10K de tableaux vides est raisonnable, le même est vrai pour des promesses là - autres bibliothèques ne pas en faire de même.
Je suis moins inquiet au sujet de la taille de la mémoire d'une promesse unique (et Bluebird n'incroyable bien sûr là), mais que 10K promesses devra être alloué (en même temps). La croissance de la mémoire pour le résoudre de la chaîne n'est pas nécessaire (comme c'est le besoin de le configurer 10K de promesses de "satisfait" de l'état à la fois).
Oui, ce modèle est exactement comme un
while
boucle, avec un asynchrones corps. C'est pourquoi il est utile 🙂Réponse a accepté, avec remerciements. Tous ceux qui lisent ceci, veuillez également lire Benjamin Gruenbaum de réponse, ce qui ajoute une valeur supplémentaire.
OriginalL'auteur Bergi
Avertissement: l'optimisation prématurée est mauvais, le vrai moyen pour connaître les différences de rendement est de de référence de votre code, et vous ne devriez pas vous inquiéter à ce sujet (je n'ai eu droit qu'à une fois et j'ai profité des promesses d'au moins 100 projets).
Oui, les promesses aurait à "se souvenir" de ce qu'ils sont, si vous faites cela pour 10000 promesses que vous auriez un 10000 long de la promesse de la chaîne, si vous n'avez pas alors vous ne serez pas (par exemple, avec la récursivité) - cela est vrai pour toute la mise en attente du contrôle de flux.
Si vous devez garder une trace de 10000 choses supplémentaires (les opérations) alors vous devez garder en mémoire et qui prend du temps, si ce nombre est de un million, il pourrait ne pas être viable. Cela varie entre les bibliothèques.
Bien sûr, c'est un gros problème, et un cas d'utilisation pour l'utilisation de quelque chose comme
Promise.each
dans les bibliothèques comme bluebird surthen
en mesure de chaînage.J'ai eu personnellement dans mon code pour éviter ce style pour une rapide application qui parcourt tous les fichiers dans une machine virtuelle une fois - mais dans la grande majorité des cas, c'est un non-problème.
Oui, énormément. Par exemple bluebird 3.0 ne sera pas allouer un supplément de file d'attente si l'on détecte une promesse opération est déjà asynchrone (par exemple si elle commence avec une Promesse.retard) et exécuter simplement les choses de manière synchrone (parce que l'async garanties sont déjà conservés).
Cela signifie que ce que j'ai demandé dans ma réponse à la première question n'est pas toujours vrai (mais c'est vrai dans l'utilisation régulière de cas) 🙂 Natif promesses ne seront jamais en mesure de le faire, à moins de soutien interne est fourni.
Puis de nouveau - c'est pas une surprise puisque la promesse bibliothèques diffèrent de plusieurs ordres de grandeur à partir de l'un de l'autre.
Merci @Bergi :). Par la façon dont il semble que bluebird est-ce à environ 100 fois plus rapide que les promesses. Vérifié sur jsperf (jsperf.com/native-promises-vs-recursion-chaining - supprimer le bluebird ligne de script et de comparer)
Notez que
Promise.each
ne couvre pas la récursivité.Benjamin, merci pour votre ULTRA-rapide réponse. Je suis en attente de Bergi promis de réponse, alors, sera sans aucun doute avoir à prendre une décision difficile qui reçoit la coche verte. AFIN que vous puissiez toujours compter sur vous deux pour la qualité des réponses.
merci pour les compléments - je ne me soucie pas vraiment sur la coche verte. Je viens de me rappeler avoir ce problème et de la question il y a quelques semaines j'ai donc pris "que". Je suis juste heureux que j'ai Bergi pour s'occuper de la balise 🙂
OriginalL'auteur Benjamin Gruenbaum
Je vient de sortir un hack qui peut aider à résoudre le problème: ne pas faire de la récursivité dans la dernière
then
, plutôt, de le faire dans la dernièrecatch
, depuiscatch
est hors de la volonté de la chaîne. À l'aide de votre exemple, il serait comme ceci:Maintenant, nous avons tiré une conclusion à travers la discussion, pourquoi ne pas aller plus loin pour résoudre le problème? J'ai moi-même rencontré le problème, j'ai donc atteint cette page par le biais de la recherche, il se peut que d'autres lecteurs au sujet de la façon de résoudre le problème, pourquoi ne pas les laisser trouver la réponse ici? Puisque c'est votre fils? C'est la communauté, vérifiez les clauses lorsque vous vous posez une question à la prochaine fois, dont le contenu appartient?
OriginalL'auteur dotslashlu
Pour compléter l'impressionnante de questions /réponses j'aimerais illustrer l'expression, qui est le résultat d'une telle asynchrone de la récursivité. Par souci de simplicité, j'ai utiliser une simple fonction qui calcule la puissance d'une donnée de base et l'exposant. La récursivité et de la base de cas sont équivalents à ceux de l'OP exemple:
Avec l'aide de certains de substitution étapes récursives partie peut être remplacée. Veuillez noter que cette expression peut être évaluée dans votre navigateur:
Interprétation:
new Promise(res => setTimeout(res, 0, 8))
l'exécuteur testamentaire est appelé immédiatement et exécute un non-bllocking calcul (imité avecsetTimeout
). Alors en suspensPromise
est retourné. C'est l'équivalent avecdoSomethingAsync()
de l'OP de l'exemple.Promise
via.then(...
. Remarque: Le corps de cette fonction de rappel a été remplacé avec le corps depowerp
.then
gestionnaire de la structure est de construire jusqu'à ce que le cas de base de la récurrence est atteint. Le cas de base renvoie unePromise
résolu avec1
.then
gestionnaire de la structure est "déroulé" en appelant les associés de rappel en conséquence.Pourquoi la structure générée imbriquées et non enchaînés? Parce que le récursive cas au sein de la
then
gestionnaires les empêche de retourner une valeur jusqu'à ce que le cas de base est atteint.Comment cela peut-il fonctionner sans pile? Les associés des rappels de former une "chaîne", dont les ponts successifs microtasks de la boucle principale.
OriginalL'auteur
Cette promesse modèle va générer une chaîne récurrente. Ainsi, chaque résoudre() permet de créer un nouveau cadre de pile (avec ses propres données), en utilisant la mémoire. Cela signifie qu'un grand nombre de enchaînés fonctions à l'aide de cette promesse modèle peut produire des erreurs de dépassement de pile.
Pour illustrer cela, je vais utiliser un petit promesse bibliothèque appelée Séquence, que j'ai écrit. Il s'appuie sur la récursivité pour parvenir à l'exécution séquentielle pour les chaînes de fonctions:
Séquence fonctionne très bien pour les petites/moyennes chaînes, dans la gamme de 0-500 fonctions. Cependant, à environ 600 chaînes de Séquence commence degradating et générant souvent des erreurs de dépassement de pile.
La ligne de fond est: actuellement, la récursivité à base de promesse bibliothèques sont plus adaptés pour les petites/moyennes fonction de chaînes, même de réduire la base de la promesse implémentations sont ok pour tous les cas, y compris les grandes chaînes.
Bien sûr, cela ne signifie pas que la récursivité à base de promesses sont mauvais. Nous avons juste besoin de les utiliser avec leurs limites dans l'esprit. Aussi, il est rare que vous aurez besoin de chaîne que de nombreux appels (>=500) par l'intermédiaire de promesses. En général, je me retrouve à l'utiliser pour async configurations qui utilisent fortement ajax. Mais même si les cas les plus complexes, je n'ai pas vu une situation avec plus de 15 chaînes.
Sur une note de côté...
Ces statistiques ont été récupérées à partir de tests réalisés avec un autre de mes bibliothèques - provisnr - qui capte le nombre d'appels de fonction à l'intérieur d'un intervalle donné de temps.
OriginalL'auteur neatsu