GHC nie zapamiętuje funkcji.
Jednak wylicza dowolne wyrażenie w kodzie co najwyżej raz na raz, gdy zostało wprowadzone otaczające je wyrażenie lambda, lub co najwyżej raz, jeśli znajduje się na najwyższym poziomie. Ustalenie, gdzie są wyrażenia lambda, może być trochę trudne, gdy używasz cukru składniowego, jak w przykładzie, więc przekonwertujmy je na równoważną składnię pozbawioną cukru:
m1' = (!!) (filter odd [1..]) -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n
(Uwaga: raport Haskell 98 faktycznie opisuje lewą sekcję operatora, podobną (a %)
do \b -> (%) a b
, ale GHC usuwa cukier (%) a
. Są one technicznie różne, ponieważ można je rozróżnić seq
. Myślę, że mogłem przesłać zgłoszenie GHC Trac na ten temat.)
Biorąc to pod uwagę, możesz zobaczyć, że w m1'
, wyrażenie filter odd [1..]
nie jest zawarte w żadnym wyrażeniu lambda, więc zostanie obliczone tylko raz na uruchomienie twojego programu, podczas gdy in m2'
, filter odd [1..]
zostanie obliczone za każdym razem, gdy zostanie wprowadzone wyrażenie lambda, tj. przy każdym wywołaniu m2'
. To wyjaśnia różnicę w czasie, który widzisz.
W rzeczywistości niektóre wersje GHC, z pewnymi opcjami optymalizacji, będą miały więcej wspólnych wartości, niż wskazuje powyższy opis. W niektórych sytuacjach może to być problematyczne. Na przykład rozważmy funkcję
f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])
GHC może zauważyć, że y
nie zależy od funkcji x
i nie przepisuje jej na
f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])
W tym przypadku nowa wersja jest znacznie mniej wydajna, ponieważ będzie musiała odczytać około 1 GB z pamięci, w której y
jest przechowywana, podczas gdy wersja oryginalna działałaby na stałej przestrzeni i mieściłaby się w pamięci podręcznej procesora. W rzeczywistości w GHC 6.12.1 funkcja f
jest prawie dwa razy szybsza, gdy jest kompilowana bez optymalizacji, niż jest kompilowana z -O2
.
seq
m1 10000000). Istnieje jednak różnica, gdy nie określono flagi optymalizacji. Nawiasem mówiąc, oba warianty twojego "f" mają maksymalną rezystancję 5356 bajtów niezależnie od optymalizacji (z mniejszą całkowitą alokacją, gdy używane jest -O2).f
:main = interact $ unlines . (show . map f . read) . lines
; kompilować z lub bez-O2
; wtedyecho 1 | ./main
. Jeśli napiszesz test typumain = print (f 5)
,y
może być zbierany jako śmieci, ponieważ jest używany i nie ma różnicy między tymi dwomaf
.map (show . f . read)
, oczywiście. A teraz, gdy pobrałem GHC 6.12.3, widzę te same wyniki, co w GHC 6.12.1. I tak, masz rację co do oryginałum1
im2
: wersje GHC, które wykonują ten rodzaj podnoszenia z włączonymi optymalizacjami, zmienią sięm2
wm1
.m1 jest obliczane tylko raz, ponieważ jest stałą formą aplikacyjną, podczas gdy m2 nie jest CAF, więc jest obliczane dla każdej oceny.
Zobacz wiki GHC na temat CAF: http://www.haskell.org/haskellwiki/Constant_applicative_form
źródło
[1 ..]
jest obliczana tylko raz podczas wykonywania programu, czy też jest obliczana raz na aplikację funkcji, ale czy jest związana z CAF?m1
jest to CAF, druga ma zastosowanie ifilter odd [1..]
(nie tylko[1..]
!) Jest obliczana tylko raz. GHC może również zauważyć, żem2
odnosifilter odd [1..]
się do tego samego elementu, w którym jest używany, i umieścić do niego łączem1
, ale byłby to zły pomysł: w niektórych sytuacjach może to prowadzić do dużych wycieków pamięci.[1..]
ifilter odd [1..]
. Co do reszty, nadal nie jestem przekonany. Jeśli się nie mylę, CAF ma znaczenie tylko gdy chcemy twierdzić, że kompilator mógłby zastąpićfilter odd [1..]
wm2
przez globalną thunk (którym może być nawet takie same thunk, który wykorzystano wm1
). Ale w sytuacji pytającego kompilator nie dokonał tej „optymalizacji” i nie widzę jej związku z pytaniem.m1
, i to robi.Istnieje zasadnicza różnica między tymi dwiema postaciami: ograniczenie monomorfizmu dotyczy m1, ale nie m2, ponieważ m2 wyraźnie podał argumenty. Więc typ m2 jest ogólny, ale m1 jest specyficzny. Przypisane im typy to:
Większość kompilatorów i interpreterów Haskell (wszystkie, które znam) nie zapamiętuje struktur polimorficznych, więc wewnętrzna lista m2 jest odtwarzana za każdym razem, gdy jest wywoływana, a m1 nie.
źródło
Nie jestem pewien, ponieważ sam jestem całkiem nowy w Haskellu, ale wydaje się, że to dlatego, że druga funkcja jest sparametryzowana, a pierwsza nie. Charakter funkcji polega na tym, że jej wynik zależy od wartości wejściowej, aw paradygmacie funkcjonalnym, w szczególności TYLKO od wejścia. Oczywistą implikacją jest to, że funkcja bez parametrów zwraca zawsze tę samą wartość, bez względu na wszystko.
Najwyraźniej w kompilatorze GHC istnieje mechanizm optymalizujący, który wykorzystuje ten fakt do obliczenia wartości takiej funkcji tylko raz dla całego czasu wykonywania programu. Z pewnością robi to leniwie, ale mimo to robi to. Sam to zauważyłem, pisząc następującą funkcję:
Następnie przetestować go, wszedłem GHCI i napisał:
primes !! 1000
. Minęło kilka sekund, ale w końcu dostałem odpowiedź:7927
. Potem zadzwoniłemprimes !! 1001
i natychmiast otrzymałem odpowiedź. Podobnie w jednej chwili otrzymałem wynik dlatake 1000 primes
, ponieważ Haskell musiał obliczyć całą listę tysięcy elementów, aby wcześniej zwrócić 1001 element.Tak więc, jeśli możesz napisać swoją funkcję w taki sposób, że nie przyjmuje parametrów, prawdopodobnie tego chcesz. ;)
źródło