Est la récursivité jamais plus vite que la boucle?
Je sais que la récursivité est parfois beaucoup plus propre que en boucle, et je ne demande rien à ce sujet quand je dois utiliser la récursivité sur itération, je sais qu'il ya beaucoup de questions à ce sujet déjà.
Ce que je demande est, est la récursivité jamais plus rapide qu'une boucle? Pour moi, il semble, sera toujours à être en mesure de préciser une boucle et qu'il puisse effectuer plus rapidement qu'une fonction récursive car la boucle est absent constamment mise en place de nouveaux stack frames.
Je suis à la recherche précisément si la récursivité est plus rapide dans les applications où la récursivité est la bonne façon de traiter les données, comme dans certaines fonctions de tri, dans les arbres binaires, etc.
En ce qui me concerne, je préfère de beaucoup d'itération. 😉
double possible de la Récursivité ou d'Itération?
voir stackoverflow.com/questions/2651112/...
OriginalL'auteur Carson Myers | 2010-04-16
Vous devez vous connecter pour publier un commentaire.
Cela dépend de la langue utilisée. Vous avez écrit " langue-agnostique, donc je vais donner quelques exemples.
En Java, C, Python, la récursivité est assez cher par rapport à l'itération (en général), car il nécessite l'attribution d'un nouveau cadre de pile. Dans certains compilateurs C, on peut utiliser un compilateur drapeau pour éliminer cette surcharge de travail, qui transforme certains types de récursivité (en fait, certains types d'appels tail) dans les sauts à la place des appels de fonction.
En langage de programmation fonctionnel implémentations, parfois, l'itération peut être très coûteux et la récursivité peut être très bon marché. Dans de nombreux, la récursivité est transformé en un simple saut, mais le changement de la variable de boucle (qui est mutable) parfois nécessite une certaine relativement lourd des opérations, en particulier sur des implémentations qui charge plusieurs threads d'exécution. La Mutation est cher dans certains de ces milieux, en raison de l'interaction entre le mutateur et le garbage collector, si les deux peuvent être en cours d'exécution en même temps.
Je sais que dans certains Régime implémentations, la récursivité sera généralement plus rapide que la boucle.
En bref, la réponse dépend du code et de la mise en œuvre. Utiliser quelque soit le style que vous préférez. Si vous êtes en utilisant un langage fonctionnel, la récursivité pourrait être plus rapide. Si vous êtes en utilisant un langage impératif, l'itération est probablement plus rapide. Dans certains environnements, les deux méthodes aboutissent au même assemblée généré (mettre cela dans votre pipe et la fumée).
Addendum: Dans certains environnements, la meilleure alternative est ni la récursivité, ni itération, mais plutôt des fonctions d'ordre supérieur. Ces derniers comprennent "carte", "filtre", et "réduire" (qui est aussi appelé "fold"). Non seulement ces le style préféré, non seulement ils sont souvent plus propre, mais dans certains environnements, ces fonctions sont le premier (ou le seul) pour obtenir un coup de pouce de la parallélisation automatique — de sorte qu'ils peuvent être beaucoup plus rapides que soit l'itération ou la récursivité. Parallèle des données Haskell est un exemple d'un tel environnement.
Interprétations de la liste sont une autre alternative, mais ils sont généralement juste sucre syntaxique pour l'itération, la récursivité, ou des fonctions d'ordre supérieur.
Aussi, la récursivité est, en général, plus l'approche naturelle dans les langages fonctionnels, et l'itération est normalement plus intuitive dans les langages impératifs. La différence de performance est peu probable pour être perceptible, donc il suffit d'utiliser tout ce qui se sent de plus naturel pour cette langue en particulier. Par exemple, vous ne souhaitez probablement pas à l'utilisation de l'itération en Haskell lorsque la récursion est beaucoup plus simple.
Généralement, la récursivité est compilé pour les boucles, les boucles étant d'un niveau inférieur à construire. Pourquoi? Parce que la récursivité est généralement fondé sur certaines données de la structure, induisant une Initiale F-algèbre et vous permettant de prouver des propriétés relatives à la résiliation avec inductive des arguments au sujet de la structure de l' (récursif) le calcul. Le processus par lequel la récursivité est compilé à boucles est la queue d'appel d'optimisation.
Ce qui importe le plus, c'est des opérations non effectuées. Plus vous "IO", plus vous avez à traiter. Onu-IOing de données (aka indexation) est toujours le plus gros gain de performances de tout système parce que vous n'avez pas à traiter en premier lieu.
OriginalL'auteur Dietrich Epp
Non, Itération sera toujours plus rapide que la Récursivité. (dans une Architecture de Von Neumann)
Explication:
Si vous construisez un minimum d'opérations d'un générique de l'ordinateur à partir de zéro, "Itération" vient d'abord comme un bloc de construction et exigeant moins de ressources "récursivité", ergo est plus rapide.
La construction d'un pseudo-informatique-machine à partir de zéro:
Vous remettre en Question: de Quoi avez-vous besoin de calculer une valeur, c'est à dire de suivre un algorithme, et de parvenir à un résultat?
Nous permettra d'établir une hiérarchie de concepts, à partir de zéro et de définir en premier lieu la base, concepts de base, puis de construction de second niveau des concepts avec ceux-ci, et ainsi de suite.
Premier Concept: cellules de Mémoire, de stockage, de l'État. Faire quelque chose que vous devez lieux pour stocker des intermédiaires et finaux valeurs de résultat. Imaginons que nous avons un éventail infini de "integer" cellules, appelées Mémoire, M[0..Infini].
Instructions: faire quelque chose de transformation d'une cellule, de modifier sa valeur. modifier l'état. Chaque intéressante instruction effectue une transformation. Les instructions de base sont:
a) Set & déplacer des cellules de mémoire
b) de la Logique et de l'arithmétique
Un Agent d'Exécution: un de base dans un cadre moderne CPU. Un "agent" est quelque chose qui peut exécuter des instructions. Un Agent peut également être une personne à la suite de l'algorithme sur papier.
Ordre des étapes: une séquence d'instructions: c'est à dire: d'abord faire cela, faire cela après, etc. Un impératif séquence d'instructions. Une ligne expressions sont "un impératif séquence d'instructions". Si vous avez une expression avec un "ordre de l'évaluation" alors vous avez étapes. Cela signifie que même un seul composé expression implicite “étapes” et dispose également d'un local implicite variable (appelons-le “résultat”). par exemple:
L'expression ci-dessus implique 3 étapes avec un implicite "résultat" à la variable.
Donc même infix expressions, puisque vous avez un ordre spécifique de l'évaluation, sont un impératif séquence d'instructions. L'expression implique une séquence d'opérations à faire dans un ordre précis, et parce qu'il y a étapes, il y a aussi un implicite "résultat" variable intermédiaire.
Pointeur d'Instruction: Si vous avez une séquence d'étapes, vous avez également un implicite "pointeur d'instruction". Le pointeur d'instruction des marques de la prochaine instruction, et les avances après l'instruction de lecture, mais avant l'instruction est exécutée.
Dans cette pseudo-informatique-machine, le Pointeur d'Instruction est une partie de Mémoire. (Remarque: Normalement, le Pointeur d'Instruction sera un “registre spécial” dans un cœur de PROCESSEUR, mais ici, nous allons simplifier les concepts et d'en assumer toutes les données (registres inclus) font partie de la “Mémoire”)
Sauter - une Fois que vous avez commandé un certain nombre d'étapes et un Pointeur d'Instruction, vous pouvez appliquer la "magasin" instruction pour modifier la valeur du Pointeur d'Instruction lui-même. Nous appellerons cette utilisation spécifique de la magasin instruction avec un nouveau nom: Sauter. Nous utilisons un nouveau nom, car il est plus facile de penser à elle comme un nouveau concept. En modifiant le pointeur d'instruction nous allons demander à l'agent de “passer à l'étape x“.
Infini Itération: Par sauter en arrière, maintenant, vous pouvez faire de l'agent "répéter" un certain nombre d'étapes. À ce stade, nous avons infini Itération.
Conditionnelle - Conditionnelle de l'exécution d'instructions. Avec le "conditionnel" clause, vous pouvez exécuter conditionnellement l'un de plusieurs instructions fondées sur l'état actuel (qui peut être définie avec une instruction précédente).
Bon Itération: Maintenant, avec la conditionnelle clause, nous pouvons échapper à la boucle infinie de la revenir instruction. Nous avons maintenant un conditionnelle boucle et puis bon Itération
De nommage: donner des noms à un emplacement de mémoire contenant des données ou la tenue d'une étape. C'est juste une "commodité" pour avoir. Nous n'avons pas ajouter de nouvelles instructions en ayant la capacité de définir des “noms” pour les emplacements de mémoire. “Naming” n'est pas une instruction de l'agent, c'est juste un confort pour nous. De nommage rend le code (à ce point) plus facile à lire et plus facile à modifier.
Un niveau de sous-routine: Supposons qu'il y a une série d'étapes que vous devez exécuter fréquemment. Vous pouvez stocker les étapes dans un nom de position en mémoire, puis saut à cette position lorsque vous avez besoin de les exécuter (appel). À la fin de la séquence, vous aurez besoin de retour au point de appel pour poursuivre l'exécution. Avec ce mécanisme, vous êtes la création de nouvelles instructions (sous-programmes) par la composition de base des instructions.
Mise en œuvre: (pas de nouveaux concepts requis)
Problème avec le un niveau mise en œuvre: Vous ne pouvez pas appeler un autre sous-routine à partir d'un sous-programme. Si vous le faites, vous allez écraser l'adresse de retour (variable globale), de sorte que vous ne pouvez pas imbriquer des appels.
D'avoir un une meilleure mise en Œuvre pour les sous-routines: Vous avez besoin d'une PILE
Pile: Vous définissez un espace de mémoire pour fonctionner comme une "pile", vous pouvez “pousser” des valeurs sur la pile, et aussi “pop” le dernier “poussé” de la valeur. Pour mettre en œuvre une pile, vous aurez besoin d'un Pointeur de Pile (similaire au Pointeur d'Instruction), ce qui souligne la “tête” de la pile. Lorsque vous “pousser” une valeur, le pointeur de pile décrémente et vous stocker la valeur. Lorsque vous “pop”, vous obtenez la valeur au Pointeur de Pile, puis le Pointeur de Pile est incrémenté.
Sous-routines Maintenant que nous avons un pile nous pouvons mettre en œuvre correcte des sous-routines permettant les appels imbriqués. La mise en œuvre est similaire, mais au lieu de stocker le Pointeur d'Instruction dans un prédéfinie position de mémoire, nous "pousser" la valeur de la propriété intellectuelle dans le pile. À la fin de la sous-routine, nous venons de “pop”, la valeur de la pile, de manière efficace saut de retour à l'instruction après l'original appel. Cette mise en œuvre, d'avoir une “pile” permet d'appeler un sous-programme à partir d'un autre sous-programme. Avec cette application, nous pouvons créer plusieurs niveaux d'abstraction lors de la définition de de nouvelles instructions comme des sous-programmes, en utilisant la base des instructions ou des autres sous-programmes, comme les blocs de construction.
La récursivité: Ce qui se passe quand un sous-programme s'appelle elle-même?. Ceci est appelé la "récursivité".
Problème: d'Écraser les locaux de résultats intermédiaires d'un sous-programme peut être stocker dans la mémoire. Depuis que vous appelez de réutiliser les mêmes étapes, si les résultats intermédiaires sont stockées dans prédéfinis emplacements de mémoire (variables globales), ils seront remplacés sur les appels imbriqués.
Solution: Pour permettre la récursivité, les sous-programmes doivent magasin local de résultats intermédiaires dans la pile, par conséquent, sur chaque appel récursif (directe ou indirecte), les résultats intermédiaires sont stockés dans différents emplacements de mémoire.
...
avoir atteint la récursivité nous arrêter ici.
Conclusion:
Dans une Architecture de Von Neumann, clairement "Itération" est plus simple/le concept de base de “Récursivité". Nous avons une forme de "Itération" au niveau 7, tandis que "Récursivité" est au niveau 14 de la hiérarchie de concepts.
Itération sera toujours plus rapide en code machine, car elle implique moins d'instructions, par conséquent, moins de cycles de PROCESSEUR.
Lequel est le "mieux"?
Vous devez utiliser "itération" lors du traitement de données simple, séquentielle des structures de données, et partout une “simple boucle” on va le faire.
Vous devez utiliser "récursivité" quand vous en avez besoin pour traiter une structure de données récursive (j'aime à les appeler “Fractal de Structures de Données”), ou lorsque la solution récursive est clairement plus “élégant”.
Conseils: utiliser le meilleur outil pour le travail, mais de comprendre le fonctionnement interne de chaque outil afin de choisir judicieusement.
Enfin, notez que vous avez beaucoup de possibilités d'utiliser la récursivité. Vous avez Récursive des Structures de Données partout, vous êtes à la recherche à la une aujourd'hui: les parties du DOM d'appuyer ce que vous lisez sont un RDS, un JSON expression est un RDS, le système de fichiers hiérarchique dans votre ordinateur est un RDS, j'.e: vous avez un répertoire racine, contenant des fichiers et des répertoires, chaque répertoire contenant des fichiers et des répertoires, chacun de ces répertoires contenant des fichiers et des répertoires...
"la récursivité peut être transformé en un saut" est faux. Vraiment utile de récursivité ne peut pas être transformé en un saut. Appel Tail "récursivité" est un cas spécial, où vous code "comme la récursivité" quelque chose qui peut être simplifié dans une boucle par le compilateur. Aussi, vous êtes à l'amalgame entre "immuable" avec "récursivité", ceux qui sont orthogonaux concepts.
"Est-il réellement utile de récursivité ne peut pas être transformé en un saut" -> donc la queue d'appel d'optimisation est en quelque sorte inutile? Aussi, immuable et la récursivité peut être orthogonale, mais vous n'lien en boucle avec mutable compteurs - regardez votre étape 9. Me semble que vous êtes en train de penser que la lecture en boucle et la récursivité sont radicalement différents concepts; ils ne le sont pas. stackoverflow.com/questions/2651112/...
OriginalL'auteur Lucio M. Tato
La récursivité peut-être bien plus vite au cas où l'alternative est explicitement gérer une pile, comme dans le tri ou arbre binaire algorithmes que vous mentionnez.
J'ai eu un cas où la réécriture d'un algorithme récursif en Java fait, il est plus lent.
De sorte que la bonne approche est d'abord l'écrire dans la manière la plus naturelle, uniquement optimiser si le profilage montre qu'il est essentiel, et puis de mesurer la prétendue amélioration.
+1 pour reconnaître que le matériel de la pile peut être plus rapide qu'un logiciel, manuellement mis en œuvre dans des tas pile. Effectivement, montrant que tous les "non" les réponses sont incorrectes.
OriginalL'auteur starblue
La queue de la récursivité est aussi rapide que de faire la boucle. De nombreux langages fonctionnels ont la queue de la récursivité mis en œuvre en eux.
OriginalL'auteur mkorpela
Considérer ce qui doit absolument être fait pour chacun, l'itération et la récursivité.
Vous voyez qu'il n'y a pas beaucoup de place pour les différences ici.
(Je suppose que la récursivité être une queue-appel de compilateur et d'être conscient de que l'optimisation).
OriginalL'auteur Pasi Savolainen
Plus de réponses ici, oubliez le coupable évident pourquoi la récursivité est souvent plus lent que itératif des solutions. C'est lié à l'accumulation et à la déchirure en bas de la pile des images, mais n'est pas exactement cela. C'est généralement une grande différence dans le stockage de l'auto variable pour chaque récursion. Dans un algorithme itératif avec une boucle, les variables sont souvent détenus dans des registres, et même si on déborde, ils se trouvent dans le cache de Niveau 1. Dans un algorithme récursif, tous les états intermédiaires de la variable sont stockées sur la pile, ce qui signifie qu'ils engendrent beaucoup plus de déversements de mémoire. Cela signifie que même si elle fait la même quantité d'opérations, elle aura beaucoup les accès à la mémoire dans la chaleur de la boucle et ce qui est pire, ces opérations de mémoire ont un mauvais taux de réutilisation faire les caches moins efficace.
TL;DR algorithmes récursifs ont généralement un plus mauvais comportement de cache que itératif.
OriginalL'auteur Patrick Schlüter
La plupart des réponses ici sont mal. La bonne réponse est ça dépend. Par exemple, ici, sont deux fonctions C qui marche par l'intermédiaire d'un arbre. D'abord le récursive:
Et c'est ici la même fonction implémentée à l'aide de l'itération:
Il n'est pas important de comprendre les détails du code. Juste que
p
sont des nœuds et queP_FOR_EACH_CHILD
la marche. Dans la version itérative nous avons besoin d'une pile explicitest
sur lequel les nœuds sont poussés et ensuite éclatés et manipulé.La fonction récursive tourne beaucoup plus vite que l'itératif. La raison en est que dans le second, pour chaque élément, un
CALL
à la fonctionst_push
est nécessaire, puis une autre àst_pop
.Dans le premier cas, vous n'avez le récursive
CALL
pour chaque nœud.De Plus, l'accès aux variables sur la pile des appels est incroyablement rapide. Cela signifie que vous êtes lecture de la mémoire qui est susceptible d'être toujours au plus profond de la mémoire cache. Explicite d'une pile, d'autre part, doit être soutenue par
malloc
:ed de la mémoire dans le tas qui est beaucoup plus lent à l'accès.Avec l'optimisation minutieuse, comme l'in-lining
st_push
etst_pop
, je peux atteindre à peu près de la parité avec l'approche récursive. Mais au moins, sur mon ordinateur, le coût d'accès à la mémoire du tas est plus grand que le coût de l'appel récursif.Mais cette discussion est tout à fait discutable, car récursive de l'arbre de la marche est incorrect. Si vous avez un assez grand arbre, vous serez à court de pile des appels de l'espace qui est pourquoi un algorithme itératif doit être utilisé.
Fait une pré-commande de la traversée de 7 nœud de l'arbre binaire de 10^8 fois. La récursivité 25ns. Explicite de la pile (bound-cochée ou pas -- ne prend pas beaucoup de différence) ~ 15ns. La récursivité doit faire plus (enregistrer la sauvegarde et la restauration + (généralement) plus strict cadre des alignements), en plus de simplement pousser et de sauter. (Et il s'aggrave avec la PLT dans dynamiquement lié libs.) Vous n'avez pas besoin de tas-d'allouer de manière explicite de la pile. Vous pouvez faire un obstack dont la première image est la pile d'appel de sorte que vous n'avez pas à sacrifier la localité de cache pour le cas le plus courant où vous ne dépassez pas le premier bloc.
OriginalL'auteur Björn Lindqvist
Réaliste du système, non, la création d'un cadre de pile sera toujours plus cher qu'un INC et un JMP. C'est pourquoi, vraiment bon, les compilateurs de transformer automatiquement la queue de la récursivité en appel à la même image, c'est à dire sans les frais généraux, de sorte que vous obtenez le plus lisible de la source de la version et la plus efficace de la version compilée. Un vraiment, vraiment bon compilateur devrait même être en mesure de transformer la normale de la récursivité dans la queue de la récursivité où c'est possible.
OriginalL'auteur Kilian Foth
De la programmation fonctionnelle est plus à propos de "ce" plutôt que "comment".
La langue des réalisateurs vont trouver un moyen d'optimiser le fonctionnement du code en dessous, si nous n'essayons pas de le rendre plus optimisé qu'il doit être. La récursivité peut également être optimisée au sein de l'langues queue appel d'optimisation.
Ce qui compte le plus d'un programmeur point de vue est la lisibilité et la maintenabilité plutôt que de l'optimisation en premier lieu. Encore une fois, "l'optimisation prématurée est la racine de tout mal".
OriginalL'auteur noego
C'est une supposition. Généralement la récursivité n'a probablement pas battre en boucle souvent ou jamais sur des problèmes de taille décente, si les deux utilisent vraiment bon algorithmes(en ne comptant pas la mise en œuvre de la difficulté) , il peut être différent si elle est utilisée avec une langue w/queue appelons récursivité(et une queue algorithme récursif et avec des noeuds dans le cadre de la langue)-qui aurait probablement très similaire et peut-être même préférer la récursivité de temps en temps.
OriginalL'auteur Roman A. Taycher
En général, non, la récursivité ne sera pas plus rapide qu'une boucle réaliste de l'utilisation qui est viable implémentations dans les deux formes. Je veux dire, bien sûr, vous pourriez code des boucles qui prennent une éternité, mais il y aurait de meilleures façons de mettre en œuvre la même boucle qui pourrait surpasser tout de la mise en œuvre du même problème par le biais de la récursivité.
Vous frappez le clou sur la tête quant à la raison; la création et la destruction de la pile des cadres est plus cher qu'un simple saut.
Toutefois, notez que j'ai dit "a viable implémentations dans les deux formes". Pour des choses comme de nombreux algorithmes de tri, il a tendance à ne pas être un moyen très viable de les mettre en œuvre qui ne sont pas efficacement mis en place sa propre version d'une pile, en raison de la ponte de la enfant "tâches" qui sont intrinsèquement partie du processus. Ainsi, la récursivité peut être tout aussi rapide que de tenter de mettre en œuvre l'algorithme via une boucle.
Edit: Cette réponse est en supposant la non-fonctionnelle des langues, où la plupart des types de données de base sont mutables. Elle ne s'applique pas aux langages fonctionnels.
Yep. La queue de la récursivité peut parfois être le meilleur des deux mondes - la fonctionnellement "appropriées" afin de mettre en œuvre un appel récursif à la tâche, et la performance de l'aide d'une boucle.
Ce n'est pas, en général, c'est exact. Dans certains environnements, la mutation (qui interagit avec GC) est plus cher que la queue de la récursivité, qui est transformée en une simple boucle de la sortie, ce qui n'est pas utiliser un cadre de pile.
OriginalL'auteur Amber
Selon la théorie c'est la même choses.
La récursivité et d'une boucle avec le même O() la complexité du travail avec la même vitesse théorique, mais bien sûr de la vitesse réelle dépend de langage, le compilateur et le processeur.
Exemple avec la puissance d'un nombre peut être codée dans l'itération de façon à O(ln(n)):
O(n)
, mais un peutx
fois plus longtemps que les autres, pour tous lesn
.OriginalL'auteur Hydrophis Spiralis