web-dev-qa-db-fra.com

Quand la mémorisation est-elle automatique dans GHC Haskell?

Je ne peux pas comprendre pourquoi m1 est apparemment mémorisé alors que m2 n'est pas dans ce qui suit:

m1      = ((filter odd [1..]) !!)

m2 n    = ((filter odd [1..]) !! n)

m1 10000000 prend environ 1,5 seconde sur le premier appel, et une fraction de cela sur les appels suivants (vraisemblablement, il met en cache la liste), tandis que m2 10000000 prend toujours le même temps (reconstruire la liste à chaque appel). Une idée de ce qui se passe? Existe-t-il des règles générales pour savoir si et quand GHC mémorisera une fonction? Merci.

106
Jordan

GHC ne mémorise pas les fonctions.

Cependant, il calcule une expression donnée dans le code au plus une fois à chaque fois que son expression lambda environnante est entrée, ou au plus une fois si elle est au niveau supérieur. Déterminer où se trouvent les expressions lambda peut être un peu délicat lorsque vous utilisez du sucre syntaxique comme dans votre exemple, convertissons-les donc en syntaxe désucrée équivalente:

m1' = (!!) (filter odd [1..])              -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n

(Remarque: Le rapport Haskell 98 décrit en fait une section d'opérateur gauche comme (a %) Comme équivalent à \b -> (%) a b, mais GHC le désugarre à (%) a. Celles-ci sont techniquement différentes car elles peuvent être distingué par seq. Je pense que j'aurais peut-être soumis un ticket GHC Trac à ce sujet.)

Compte tenu de cela, vous pouvez voir que dans m1', L'expression filter odd [1..] N'est contenue dans aucune expression lambda, elle ne sera donc calculée qu'une fois par exécution de votre programme, tandis que dans m2', filter odd [1..] Sera calculé à chaque saisie de l'expression lambda, c'est-à-dire à chaque appel de m2'. Cela explique la différence de timing que vous voyez.


En fait, certaines versions de GHC, avec certaines options d'optimisation, partageront plus de valeurs que la description ci-dessus ne l'indique. Cela peut être problématique dans certaines situations. Par exemple, considérons la fonction

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])

GHC peut remarquer que y ne dépend pas de x et réécrire la fonction dans

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])

Dans ce cas, la nouvelle version est beaucoup moins efficace car elle devra lire environ 1 Go de mémoire où y est stocké, tandis que la version d'origine fonctionnerait dans un espace constant et rentrerait dans le cache du processeur. En fait, sous GHC 6.12.1, la fonction f est presque deux fois plus rapide lorsqu'elle est compilée sans optimisations qu'elle n'est compilée avec -O2.

111
Reid Barton

m1 est calculé une seule fois car il s'agit d'un formulaire d'application constant, tandis que m2 n'est pas un CAF, et est donc calculé pour chaque évaluation.

Voir le wiki du GHC sur les FAC: http://www.haskell.org/haskellwiki/Constant_applicative_form

29
sclv

Il existe une différence cruciale entre les deux formes: la restriction du monomorphisme s'applique à m1 mais pas à m2, car m2 a explicitement donné des arguments. Le type de m2 est donc général mais celui de m1 est spécifique. Les types qui leur sont attribués sont:

m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a

La plupart des compilateurs et interprètes Haskell (tous ceux que je connais en fait) ne mémorisent pas les structures polymorphes, donc la liste interne de m2 est recréée chaque fois qu'elle est appelée, où m1 ne l'est pas.

13
mokus

Je ne suis pas sûr, car je suis assez nouveau pour Haskell moi-même, mais il semble que ce soit parce que la deuxième fonction est paramétrée et la première ne l'est pas. La nature de la fonction est que son résultat dépend de la valeur d'entrée et dans le paradigme fonctionnel en particulier, cela dépend UNIQUEMENT de l'entrée. L'implication évidente est qu'une fonction sans paramètres renvoie toujours la même valeur encore et encore, quoi qu'il arrive.

Apparemment, il existe un mécanisme d'optimisation dans le compilateur GHC qui exploite ce fait pour calculer la valeur d'une telle fonction une seule fois pour l'exécution du programme entier. Il le fait paresseusement, certes, mais néanmoins. Je l'ai remarqué moi-même, quand j'ai écrit la fonction suivante:

primes = filter isPrime [2..]
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
        where f `divides` n = (n `mod` f) == 0

Puis pour le tester, je suis entré dans GHCI et j'ai écrit: primes !! 1000. Cela a pris quelques secondes, mais j'ai finalement obtenu la réponse: 7927. Ensuite, j'ai appelé primes !! 1001 et a obtenu la réponse instantanément. De même, en un instant, j'ai obtenu le résultat pour take 1000 primes, car Haskell a dû calculer toute la liste des mille éléments pour renvoyer le 1001e élément auparavant.

Ainsi, si vous pouvez écrire votre fonction de façon à ce qu'elle ne prenne aucun paramètre, vous le voulez probablement. ;)

1
Sventimir