2010-10-16 4 views
100

Io non riesco a capire il motivo per cui M1 appare memoized mentre m2 non è il seguente:Quando la memoizzazione è automatica in GHC Haskell?

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

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

m1 10000000 richiede circa 1,5 secondi in prima convocazione, e una frazione di quello sulle chiamate successive (presumibilmente memorizza nella cache la lista), mentre m2 10000000 richiede sempre lo stesso tempo (ricostruendo l'elenco ad ogni chiamata). Qualche idea su cosa sta succedendo? Esistono regole pratiche per decidere se e quando GHC memorizzerà una funzione? Grazie.

risposta

105

GHC non memorizza le funzioni.

Tuttavia, calcola ogni espressione data nel codice al massimo una volta per volta in cui viene immessa l'espressione lambda circostante, o al massimo una volta mai se è al livello superiore. Determinare dove le lambda-espressioni sono può essere un po 'difficile quando si utilizzano zucchero sintattico, come nel tuo esempio, quindi cerchiamo di convertire questi per equivalente sintassi Dezuccherato:

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

(Nota: Il rapporto Haskell 98 descrive in realtà un operatore di sinistra sezione come (a %) equivalenti a \b -> (%) a b, ma GHC desugars a (%) a. Queste sono tecnicamente differenti perché possono essere distinti da penso che potrei aver presentato un biglietto GHC Trac su questo seq..)

Detto questo, è possibile vedere che in m1', l'espressione filter odd [1..] non è contenuta n qualsiasi espressione lambda, quindi verrà calcolata una sola volta per ogni esecuzione del programma, mentre in m2', filter odd [1..] verrà calcolata ogni volta che viene immessa l'espressione lambda, ad esempio su ogni chiamata di m2'. Questo spiega la differenza di tempo che stai vedendo.


In realtà, alcune versioni di GHC, con determinate opzioni di ottimizzazione, condivideranno più valori di quanto indicato nella descrizione precedente. Questo può essere problematico in alcune situazioni. Ad esempio, si consideri la funzione

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

GHC potrebbe notare che y non dipende da x e riscrivere la funzione di

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

In questo caso, la nuova versione è molto meno efficiente perché avrà leggere circa 1 GB dalla memoria in cui è memorizzato , mentre la versione originale dovrebbe essere eseguita nello spazio costante e adattarsi alla cache del processore. In effetti, in GHC 6.12.1, la funzione f è quasi due volte più veloce quando è compilata senza le ottimizzazioni rispetto a quando è compilata con -O2.

+0

Il costo per valutare (filtro dispari [1]] è comunque vicino allo zero - è una lista lenta dopo tutto, quindi il costo reale è in (x !! 10000000) l'applicazione quando l'elenco è effettivamente valutata. Inoltre, sia m1 che m2 sembrano essere valutati una sola volta con -O2 e -O1 (sul mio ghc 6.12.3) almeno all'interno del seguente test: (test = m1 10000000 'seq' m1 10000000). C'è una differenza anche se non viene specificato alcun flag di ottimizzazione. Ed entrambe le varianti della tua "f" hanno una residenza massima di 5356 byte indipendentemente dall'ottimizzazione, a proposito (con meno allocazione totale quando viene utilizzato -O2). –

+1

@ Ed'ka: prova questo programma di test, con la precedente definizione di 'f':' main = interact $ unlines. (mostra la mappa per leggere). linee'; compilare con o senza '-O2'; quindi 'echo 1 | ./Main'. Se scrivi un test come 'main = print (f 5)', allora 'y' può essere garbage collection mentre viene usato e non c'è differenza tra i due' f's. –

+0

er, che dovrebbe essere 'map (show. F. Read)', naturalmente. E ora che ho scaricato GHC 6.12.3, vedo gli stessi risultati di GHC 6.12.1. E sì, hai ragione riguardo a 'm1' e' m2': le versioni di GHC che eseguono questo tipo di sollevamento con le ottimizzazioni attivate trasformeranno 'm2' in' m1'. –

26

m1 viene calcolato una sola volta perché è un modulo di domanda costante, mentre m2 non è un CAF, e quindi viene calcolato per ogni valutazione.

consultare il wiki GHC il CAF: http://www.haskell.org/haskellwiki/Constant_applicative_form

+0

La spiegazione "m1 è calcolata una sola volta perché è una Forma Applicativa Costante" non ha senso per me. Perché presumibilmente sia m1 che m2 sono variabili di primo livello, penso che queste funzioni siano calcolate una sola volta, indipendentemente dal fatto che siano CAF o meno. La differenza è se la lista '[1 ..]' è calcolata una sola volta durante l'esecuzione di un programma o è calcolata una volta per applicazione della funzione, ma è correlata a CAF? –

+1

Dalla pagina collegata: "Un CAF ... può essere compilato su un grafico che sarà condiviso da tutti gli usi o da un codice condiviso che si sovrascriverà con un grafico la prima volta che verrà valutato". Poiché 'm1' è un CAF, il secondo si applica e' filter odd [1 ..] '(non solo' [1 ..] '!) Viene calcolato una sola volta. GHC potrebbe anche notare che 'm2' si riferisce a' filter odd [1 ..] 'e pone un link allo stesso thunk usato in' m1', ma sarebbe una cattiva idea: potrebbe portare a grandi perdite di memoria in alcune situazioni. –

+0

@Alexey: Grazie per la correzione su '[1 ..]' e 'filter dispari [1 ..]'. Per il resto, non sono ancora convinto. Se non sbaglio, CAF è rilevante solo quando vogliamo sostenere che un compilatore _could_ sostituisce il 'filtro dispari [1 ..] 'in' m2' da un thunk globale (che può essere anche uguale a quello usato in 'm1'). Ma nella situazione del richiedente, il compilatore non ha fatto "ottimizzazione" e non riesco a vedere la sua rilevanza alla domanda. –

13

C'è una differenza fondamentale tra le due forme: la restrizione monomorfismo vale per m1, ma non M2, perché m2 ha dato esplicitamente argomenti. Quindi il tipo di m2 è generale ma quello di m1 è specifico.I tipi sono assegnati sono:

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

compilatori La maggior parte Haskell e interpreti (tutti loro, che io sappia in realtà) non Memoize strutture polimorfiche, in modo lista interna di m2 viene ricreata ogni volta che si chiama, dove M1 di non è .

+1

Giocare con questi in GHCi sembra dipendere anche dalla trasformazione let-floating (uno dei passaggi di ottimizzazione di GHC che non viene utilizzato in GHCi). E naturalmente, quando si compila queste semplici funzioni, l'ottimizzatore è in grado di farle comportare ugualmente in ogni caso (secondo alcuni test di criterio che ho eseguito comunque, con le funzioni in un modulo separato e contrassegnato con i princmi NOINLINE). Presumibilmente è perché la generazione di liste e l'indicizzazione si fondono in un ciclo molto stretto comunque. – mokus

1

Non sono sicuro, perché sono abbastanza nuovo per Haskell, ma sembra che sia la seconda funzione parametrizzata e la prima no. La natura della funzione è quella, il suo risultato dipende dal valore di input e in particolare dal paradigma funzionale dipende SOLO dall'input. Ovvia implicazione è che una funzione senza parametri restituisce sempre lo stesso valore più e più volte, non importa quale.

A parte questo c'è un meccanismo di ottimizzazione nel compilatore GHC che sfrutta questo fatto per calcolare il valore di tale funzione una sola volta per tutto il tempo di esecuzione del programma. Lo fa pigramente, per essere sicuro, ma lo fa comunque. Ho notato anch'io, quando ho scritto la seguente funzione:

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

Poi Per provarlo, sono entrato GHCI e scrissi: primes !! 1000. Ci sono voluti alcuni secondi, ma alla fine ho ricevuto la risposta: 7927. Quindi ho chiamato primes !! 1001 e ho ottenuto la risposta all'istante. Allo stesso modo in un istante ho ottenuto il risultato per take 1000 primes, perché Haskell ha dovuto calcolare l'intero elenco di migliaia di elementi per restituire l'elemento 1001st prima.

Quindi se si può scrivere la propria funzione in modo tale che non ci siano parametri, probabilmente lo si desidera. ;)