2010-10-16 30 views
100

Je ne peux pas comprendre pourquoi est apparemment memoized m1 alors que m2 est pas dans les domaines suivants:Quand la mémorisation est-elle automatique dans GHC Haskell?

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

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

prend environ 10.000.000 m1 1,5 secondes sur le premier appel, et une fraction de celle sur les appels suivants (on peut supposer qu'il met en cache la liste), tandis que m2 10000000 prend toujours le même laps de temps (reconstruire la liste avec 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.

Répondre

105

GHC ne mémorise pas les fonctions.

Cependant, il calcule une expression donnée dans le code au plus une fois par fois que son expression lambda environnante est entrée, ou tout au plus une fois si elle est au niveau supérieur. Déterminer où les expressions lambda-sont peut être un peu délicat lorsque vous utilisez du sucre syntaxique comme dans votre exemple, si nous allons convertir en syntaxe Dessucré équivalente:

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

(Note: Le rapport Haskell 98 décrit en fait un opérateur gauche section comme (a %) comme équivalent à \b -> (%) a b, mais GHC desugars à (%) a. ce sont techniquement différents parce qu'ils peuvent être distingués par seq. Je pense que je pourrais avoir soumis un billet GHC Trac à ce sujet.)

Donné ce, vous pouvez voir que dans m1', l'expression filter odd [1..] n'est pas contenue n toute expression lambda, donc elle ne sera calculée qu'une fois par exécution de votre programme, tandis que dans m2', filter odd [1..] sera calculée chaque fois que l'expression lambda est entrée, 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 ce que la description ci-dessus 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 à

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

Dans ce cas, la nouvelle version est beaucoup moins efficace parce qu'il aura lire environ 1 Go de la mémoire où y est stocké, tandis que la version d'origine s'exécuterait dans un espace constant et entrer 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.

+0

Le coût à évaluer (filtre impair [1 ..]) expression est de toute façon proche de zéro - c'est une liste paresseuse après tout, donc le coût réel est dans l'application (x !! 10000000) quand la liste est réellement évalué. En outre, m1 et m2 semblent être évalués une seule fois avec -O2 et -O1 (sur mon ghc 6.12.3) au moins dans le test suivant: (test = m1 10000000 'seq' m1 10000000). Il y a une différence quand aucun drapeau d'optimisation n'est spécifié. Et les deux variantes de votre "f" ont une résidence maximale de 5356 octets indépendamment de l'optimisation, soit dit en passant (avec moins d'allocation totale quand -O2 est utilisé). –

+1

@ Ed'ka: Essayez ce programme de test, avec la définition ci-dessus de 'f':' main = interact $ unlines. (montrer, carte f, lire). lignes »; compiler avec ou sans '-O2'; alors 'echo 1 | ./main'. Si vous écrivez un test comme 'main = print (f 5)', alors 'y' peut être récupéré comme il est utilisé et il n'y a pas de différence entre les deux' f'. –

+0

er, cela devrait être 'map (show .f read)', bien sûr. Et maintenant que j'ai téléchargé GHC 6.12.3, je vois les mêmes résultats que dans GHC 6.12.1. Et oui, vous avez raison sur les versions "m1" et "m2" d'origine: les versions de GHC qui effectuent ce type de levage avec des optimisations activées transformeront "m2" en "m1". –

26

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

Voir le wiki GHC sur CAFs: http://www.haskell.org/haskellwiki/Constant_applicative_form

+0

L'explication "m1 est calculée une seule fois parce que c'est une forme applicative constante" n'a pas de sens pour moi. Puisque vraisemblablement m1 et m2 sont des variables de premier niveau, je pense que ces fonctions ne sont calculées qu'une seule fois, peu importe qu'elles soient CAF ou non. La différence est de savoir si la liste '[1 ..]' est calculée une seule fois lors de l'exécution d'un programme ou si elle est calculée une fois par application de la fonction, mais est-elle liée à CAF? –

+1

De la page liée: "Un CAF ... peut soit être compilé à un morceau de graphique qui sera partagé par tous les usages ou à un code partagé qui va se réécrire avec un graphique la première fois qu'il est évalué". Puisque 'm1' est un CAF, le second s'applique et' filter impair [1 ..] '(et pas seulement' [1 ..] '!) Est calculé une seule fois. GHC pourrait également noter que 'm2' fait référence à' filter odd [1 ..] ', et placer un lien vers le même thème utilisé dans' m1', mais ce serait une mauvaise idée: cela pourrait conduire à de grandes fuites de mémoire dans certaines situations. –

+0

@Alexey: Merci pour la correction de '[1 ..]' et 'filter impair [1 ..]'. Pour le reste, je ne suis toujours pas convaincu. Si je ne me trompe pas, CAF n'est pertinent que si l'on veut argumenter qu'un compilateur pourrait remplacer le filtre impair [1 ..] 'dans' m2' par un thunk global (qui peut être même le même thunk que celui utilisé dans 'm1'). Mais dans la situation du demandeur, le compilateur n'a pas fait cette "optimisation", et je ne peux pas voir sa pertinence à la question. –

13

Il y a une différence essentielle entre les deux formes: la restriction de monomorphisme applique à m1, mais pas m2, parce que m2 a explicitement donné des arguments. Le type de m2 est donc général mais celui de m1 est spécifique.Les types auxquels ils sont affectés sont:

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

La plupart des compilateurs Haskell et interprètes (tous que je connais en fait) ne memoize pas des structures polymorphes, donc la liste interne de m2 est recréée à chaque fois qu'il est appelé, où m1 ce n'est pas .

+1

En jouant avec ceux-ci dans GHCi, il semble qu'il dépend aussi de la transformation let-floating (l'une des passes d'optimisation de GHC qui n'est pas utilisée dans GHCi). Et bien sûr lors de la compilation de ces fonctions simples, l'optimiseur est en mesure de les faire se comporter de façon identique (en fonction de tests de critères que j'ai exécutés de toute façon, avec les fonctions dans un module séparé et marqué avec des pragmas NOINLINE). Vraisemblablement parce que la génération de liste et l'indexation sont fusionnées dans une boucle super serrée de toute façon. – mokus

1

Je ne suis pas sûr, parce que je suis assez nouveau pour Haskell moi-même, mais il semble que c'est 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 en particulier du paradigme fonctionnel, cela dépend UNIQUEMENT de l'entrée. L'implication évidente est qu'une fonction sans paramètre retourne toujours la même valeur encore et encore, quoi qu'il arrive. Il existe clairement 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, pour être sûr, mais le fait néanmoins. Je l'ai remarqué moi-même, quand je l'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 

ensuite de le tester, je suis entré dans GHCi et écrit: primes !! 1000. Il a fallu quelques secondes, mais finalement j'ai eu la réponse: 7927. Puis j'ai appelé primes !! 1001 et j'ai obtenu la réponse instantanément. De même dans un instant, j'ai obtenu le résultat pour take 1000 primes, parce que Haskell a dû calculer toute la liste des milliers d'éléments pour retourner 1001st élément avant. Ainsi, si vous pouvez écrire votre fonction de telle sorte qu'elle ne prenne aucun paramètre, vous en aurez probablement besoin.