2

Avis de non-responsabilité: certains Haskell peut-être douteux.Haskell Fibonacci: Tail-récursif plus lent que l'approche de liste paresseux classique?

J'ai mis en 2 versions de Fibonacci, qui est linéaire et récursive (une variante claqua incl.), Et un autre en utilisant l'approche de la liste paresseuse classique:

{-# LANGUAGE BangPatterns #-} 

-- Linear 
fibLinear :: Int -> Integer 
fibLinear 0 = 0 
fibLinear 1 = 1 
fibLinear x = fibLin' 2 1 2 
    where 
     fibLin' i y z | i == x = y 
     fibLin' i y z   = fibLin' (i+1) z (z+y) 

-- Linear banged 
fibLinearBang :: Int -> Integer 
fibLinearBang 0 = 0 
fibLinearBang 1 = 1 
fibLinearBang x = fibLin' 2 1 2 
    where 
     fibLin' (!i) (!y) (!z) | i == x = y 
     fibLin' (!i) (!y) (!z)   = fibLin' (i+1) z (z+y) 

-- Haskeller classic lazy 
fibClassic :: Int -> Integer 
fibClassic n = fib' !! n 
    where fib' = 0:1:zipWith (+) fib' (tail fib') 

Lorsque vous utilisez criterion à benchmark il (code), je reçois des résultats un peu intuitifs: Fib 30 prend 443,0 ns pour fibLinear et 279,5 ns pour fibLinearBang, contre 73,51 ns pour fibClassic. C'est étrange parce que je pense que fibLinear & fibLinearBang pourrait être optimisé en quelque chose comme une boucle ou un saut.

Ma seule estimation est peut-être fib' dans fibClassic est memoised comme une liste constante malgré être dans une définition de fonction afin que les invocations ultérieures de la fonction réutiliser la même liste pour la recherche. Est-ce que quelqu'un peut me donner une explication définitive pour mes résultats de référence?

Pour ceux qui sont intéressés, j'ai mis le projet sur Github.

+2

Je voudrais essayer de rendre tout l'argument de 'fibLin'' strict et de nouveau l'indice de référence. Vous pouvez le faire, par exemple en utilisant 'fibLin '(! _) (! _) (! _) | ... 'dans le premier cas, et permettant l'extension relative' BangPatterns'. – chi

+1

Quel est votre niveau d'optimisation? La récursivité de queue ne fonctionne que comme une performance de boucle quand elle est stricte et GHC pourrait seulement rendre votre code strict à certains niveaux d'optimisation. La version paresseuse n'est définitivement pas mémo et je ne sais pas à quel point cela peut aider (vous n'avez pas besoin de recalculer le début de la liste, mais vous devez encore la parcourir et le calcul ne prend pas beaucoup de temps). C'est juste relativement rapide par défaut. – sepp2k

+1

Aussi pourquoi 'fibLin 'produit-il un tuple? Je ne vous vois jamais utiliser le deuxième élément. – sepp2k

Répondre

-1

tl; dr: fib' a en effet été réutilisé dans des invocations

Merci aux commentateurs, je l'ai appris que full-laziness est pas (seulement) un terme utilisé pour décrire la façon dont Haskell évalue, mais une optimisation du compilateur réel terme, qui est activée par défaut, mais peut être désactivé (en utilisant le drapeau -fno-full-laziness GHC-options

Relancement les points de repère avec ce drapeau, nous voyons les tables ont tourné.

  • fibLinearBang 259,8 ns
  • fibClassic 738,4 ns

Cette performance, ainsi this guide on GHC compiler options est une preuve assez convaincante que fib' ne se transforment en quelque chose comme une référence de niveau supérieur qui est réutilisé (et d'être une liste, memoised) à travers différentes invocations de la méthode. Ce guide et le paper it links to describing let-lifting admettent que let-lifting pourrait résulter en une augmentation de la mémoire résidentielle, ce qui pour moi est plutôt effrayant (n'écrivez pas un fib-as-a-service en utilisant fibClassic et exposez-le à Internet. .), mais je pourrais venir à lui ..

+0

«Une preuve assez convaincante» semble être une condition faible lorsque vous pouvez simplement * regarder le noyau * et savoir à coup sûr. Le résultat de la compilation dépend de la version du compilateur, des drapeaux, de l'optimisation, des devinettes quant à ce qui se passe et du type d'optimisation qui se traduira par de longs maux de tête comme celui-ci. Je ne sais pas pourquoi vous croyez que '-fno-full-laziness' est une 'solution' - vous avez aggravé la performance de' fibClassic' tout en n'ayant aucun changement sur celui de 'fibLinearBang'. Rendre un programme pire qu'un autre avec des drapeaux de compilateur parce que vous croyez que cela devrait être le cas semble complètement contre-productif. – user2407038

+0

"" Une preuve assez convaincante "semble une condition faible quand on peut juste regarder le noyau et savoir à coup sûr.": Le but était de savoir * pourquoi * fibClassic était (imho contre-intuitivement) battre "fibLinearBang"; le fait que «fno-full-parzness» a affecté le premier et non le dernier montre que nous avons probablement trouvé la raison. Si vous avez de meilleures preuves pour ce cas particulier que «fib» est réutilisé ou une autre raison pour expliquer l'écart de performance, peut-être commencer par fournir une réponse. Je m'intéresse à ce que vous entendez par «regardez le noyau» et comment il peut être appliqué ici. – lloydmeta

+0

Vous regardez le noyau, dans lequel vous voyez le programme qui est réellement produit par votre code, et observez que dans ce programme 'fib ':: [Integer]' est flottant au niveau supérieur, ce qui en fait un CAF (constant applicative formulaire), c'est-à-dire une copie unique sera conservée pendant la durée de l'exécution du programme. 'full laziness' ne peut pas affecter 'fibLinearBang' car même si vous deviez faire flotter manuellement la fonction au niveau supérieur, elle ne serait pas calculée moins souvent - elle n'est appelée qu'une seule fois par' fibLinearBang'. – user2407038