Ne Haskell ont récursives terminales de l'optimisation?
J'ai découvert le "temps" commande unix aujourd'hui et j'ai pensé l'utiliser pour vérifier la différence de temps d'exécution entre la queue-récursive et normal des fonctions récursives en Haskell.
J'ai écrit les fonctions suivantes:
--tail recursive
fac :: (Integral a) => a -> a
fac x = fac' x 1 where
fac' 1 y = y
fac' x y = fac' (x-1) (x*y)
--normal recursive
facSlow :: (Integral a) => a -> a
facSlow 1 = 1
facSlow x = x * facSlow (x-1)
Ceux-ci sont valables en gardant à l'esprit qu'ils l'ont été uniquement pour une utilisation avec ce projet, donc je n'ai pas pris la peine de vérifier par des zéros ou des nombres négatifs.
Cependant, lors de l'écriture d'une méthode principale pour chacun, de les compiler et de les exécuter avec le "temps" de commande, les deux avaient les mêmes moteurs d'exécution avec la normal fonction récursive bordure de la queue récursive. Ceci est contraire à ce que j'avais entendu en ce qui concerne à la queue-récursive d'optimisation en lisp. Quelle est la raison?
- Je crois que le coût total de possession est une optimisation pour économiser la pile des appels, cela ne signifie pas que vous allez économiser du temps CPU. Corrigez-moi si mal.
- N'ai pas testé avec lisp, mais le tutoriel, j'ai lu implicite que la mise en place de la pile engagés coût processeur en lui-même, tandis que la compilation-à-itératif de la queue-solution récursive ne pas dépenser toute l'énergie (en temps) de faire cela et donc plus efficace.
- eh bien cela dépend de beaucoup de choses, mais en général, les caches sont aussi en jeu, donc coût total de possession d'habitude de produire un programme plus rapide ainsi..
- Quelle est la raison? En un mot: la paresse.
- Fait intéressant, votre
fac
est plus ou moins comment ghc calculeproduct [n,n-1..1]
à l'aide d'un auxiliaire de la fonctionprod
, mais bien sûrproduct [1..n]
serait plus simple. Je ne peux que supposer qu'ils n'ont pas fait le strict dans son deuxième argument, au motif que c'est le genre de chose ghc est très confiant, il permet de compiler vers le bas pour un simple accumulateur.
Vous devez vous connecter pour publier un commentaire.
Haskell utilise paresseux-évaluation à mettre en œuvre la récursivité, donc, traite de quelque chose comme une promesse de fournir une valeur en cas de besoin (ce qui est appelé un thunk). Thunks se réduit uniquement la quantité nécessaire de procéder, sans plus. Cela ressemble à la façon de simplifier une expression mathématique, de sorte qu'il est utile de penser à elle de cette façon. Le fait que l'ordre d'évaluation est pas spécifié par votre code permet au compilateur de faire beaucoup de même plus sage d'optimisations que juste la queue-appel d'élimination que vous êtes habitué. Compiler avec
-O2
si vous voulez optimisation de la!Nous allons voir comment nous évaluons
facSlow 5
comme une étude de cas:Ainsi, tout comme vous inquiet, nous avons une accumulation de chiffres avant les calculs se produire, mais contrairement à vous inquiète, il n'y a pas de pile de
facSlow
appels de fonction suspendu autour de l'attente à la fin de chaque réduction est appliqué et s'en va, laissant un frame de pile dans son sillage (c'est parce que(*)
est stricte et donc déclenche l'évaluation de son deuxième argument).Haskell fonctions récursives ne sont pas évalués dans un très récursive façon! La seule pile des appels de traîner sont les multiplications eux-mêmes. Si
(*)
est considérée comme une stricte des données constructeur, c'est ce qui est connu comme gardé la récursivité (bien qu'il est généralement mentionné comme tel avec non-strict des données constructeurs, où ce qui est laissé dans son sillage sont les constructeurs de données - lorsque, forcé par accès).Examinons maintenant la queue-récursive
fac 5
:De sorte que vous pouvez voir comment la queue de la récursivité par lui-même n'a pas sauvé, vous avez tout le temps ou l'espace. Non seulement il prendre des mesures plus globales que
facSlow 5
, il crée également un imbriquée thunk (montré ici comme{...}
) - besoin d'un espace supplémentaire pour elle - qui décrit l'avenir de calcul, la imbriquée de multiplications à effectuer.Ce thunk est ensuite décortiquée en parcourant il vers le bas, en recréant le calcul sur la pile. Il y a aussi un risque de causer un débordement de pile avec de très longs calculs, pour les deux versions.
Si nous voulons la main-d'optimiser cela, tout ce que nous devons faire c'est de stricte. Vous pouvez utiliser l'application stricte de l'opérateur
$!
de définirCette forces
facS'
être rigoureux dans son deuxième argument. (C'est déjà le strict dans son premier argument parce que cela a à être évalués afin de décider quelle est la définition dufacS'
applique.)Parfois la rigueur peut aider énormément, parfois, c'est une grosse erreur parce que la paresse est plus efficace. Ici c'est une bonne idée:
Qui est ce que tu voulais faire, je pense.
Résumé
-O2
foldr
etfoldl
par exemple, et de les tester les uns contre les autres.Essayer ces deux:
foldl1
est la queue récursive, alors quefoldr1
effectue gardé la récursivité de sorte que le premier élément est immédiatement présenté pour la poursuite du traitement/de l'accès. (Le premier "parenthesizes" de la gauche à la fois,(...((s+s)+s)+...)+s
, forçant sa liste d'entrée pleinement à sa fin et la construction d'un grand sens de l'avenir de calcul beaucoup plus tôt que ses résultats complets sont nécessaires; la deuxième parenthesizes pour le droit progressivement,s+(s+(...+(s+s)...))
, la consommation de la liste d'entrée peu à peu, donc, le tout est de pouvoir fonctionner dans de la constante de l'espace, avec les optimisations).Vous pourriez avoir besoin d'ajuster le nombre de zéros en fonction de ce matériel que vous utilisez.
Il convient de mentionner que le
fac
fonction n'est pas un bon candidat pour gardé la récursivité. La queue de la récursivité est la voie à suivre ici. En raison de la paresse vous n'êtes pas obtenir l'effet de coût total de possession de votrefac'
fonction parce que l'accumulateur arguments garder dans la construction de grands thunks, qui lors de l'évaluation nécessitera une énorme pile. Pour éviter cela et d'obtenir l'effet souhaité, TCO vous avez besoin de faire ces accumulateur arguments stricte.Si vous compiler à l'aide de
-O2
(ou juste-O
) GHC sera probablement le faire sur son propre dans la la rigueur de l'analyse phase.$!
qu'avecBangPatterns
, mais c'est une bonne réponse. En particulier la mention de la rigueur de l'analyse.Vous devriez consulter l'article de wiki sur la queue de la récursivité en Haskell. En particulier, en raison de l'évaluation de l'expression, le genre de la récursivité vous voulez, c'est gardé la récursivité. Si vous travailler sur les détails de ce qui se passe sous le capot (dans la machine abstraite pour Haskell), vous obtenez le même genre de chose qu'avec de la queue de la récursivité dans le strict langues. Parallèlement à cela, vous avez une syntaxe uniforme pour les paresseux fonctions (queue de récursivité permettra de vous lier à une évaluation rigoureuse, alors que gardé la récursivité fonctionne plus naturellement).
(Et dans l'apprentissage de Haskell, le reste des pages du wiki sont génial, trop!)
Si je me souviens bien, GHC optimise automatiquement la plaine des fonctions récursives en queue-récursive optimisées pour les.