Poniżej podano 3 funkcje, które znajdują ostatni, ale drugi element na liście. Ten, który używa last . init
wydaje się znacznie szybszy niż reszta. Nie mogę zrozumieć, dlaczego.
Do testów wykorzystałem listę danych wejściowych [1..100000000]
(100 milionów). Ostatni działa prawie natychmiast, a pozostałe trwają kilka sekund.
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
init
został zoptymalizowany, aby uniknąć wielokrotnego „rozpakowywania” listy.myButLast
znacznie wolniejszy ?. Wygląda na to, że nie rozpakowuje żadnej listy, ale po prostu przemierza ją, tak jakinit
funkcja ...[x, y]
to skrót(x:(y:[]))
, więc rozpakowuje zewnętrzne wady, drugie wady i sprawdza, czy ogon drugiegocons
jest[]
. Ponadto druga klauzula ponownie rozpakuje listę(x:xs)
. Tak, rozpakowywanie jest dość wydajne, ale oczywiście, jeśli zdarza się to bardzo często, spowalnia proces.init
nie sprawdza wielokrotnie, czy jej argumentem jest lista singletonów czy pusta lista. Po rozpoczęciu rekurencji zakłada się, że pierwszy element zostanie przypięty do wyniku wywołania rekurencyjnego.myButLast
automatycznie. Myślę, że bardziej prawdopodobne jest, że fuzja listy będzie winna za przyspieszenie.Odpowiedzi:
Studiowanie szybkości i optymalizacji jest bardzo łatwe uzyskać bardzo błędne wyniki . W szczególności nie można tak naprawdę powiedzieć, że jeden wariant jest szybszy od drugiego, nie wspominając o wersji kompilatora i trybie optymalizacji konfiguracji testów porównawczych. Nawet wtedy nowoczesne procesory są tak wyrafinowane, że zawierają predyktory gałęzi oparte na sieci neuronowej, nie wspominając już o wszelkiego rodzaju pamięciach podręcznych, więc nawet przy starannej konfiguracji wyniki testów porównawczych będą rozmyte.
Biorąc to pod uwagę ...
Benchmarking jest naszym przyjacielem.
criterion
to pakiet, który zapewnia zaawansowane narzędzia do testów porównawczych. Szybko opracowałem taki test porównawczy:Jak widzisz, dodałem wariant, który wyraźnie pasuje do dwóch elementów jednocześnie, ale poza tym jest to ten sam kod, dosłownie. Testy porównawcze przeprowadzam również w odwrotnej kolejności, aby mieć świadomość stronniczości wynikającej z buforowania. Więc biegnijmy i zobaczmy!
Wygląda na to, że nasza „wolna” wersja wcale nie jest wolna! A zawiłości dopasowywania wzorów nic nie dodają. (Nieznaczne przyspieszenie widzimy między dwoma kolejnymi seriami
match2
I przypisuję efekt buforowania).Istnieje sposób na uzyskanie większej liczby „naukowych” danych: możemy
-ddump-simpl
i przyjrzeć się sposobowi, w jaki kompilator widzi nasz kod.Kontrola konstrukcji pośrednich jest naszym przyjacielem.
„Rdzeń” to wewnętrzny język GHC. Każdy plik źródłowy Haskell jest uproszczony do Core, zanim zostanie przekształcony w końcowy wykres funkcjonalny do wykonania przez system czasu wykonywania. Jeśli spojrzymy na ten etap pośredni, powie nam to
myButLast
ibutLast2
są równoważne. Potrzeba patrzenia, ponieważ na etapie zmiany nazwy wszystkie nasze ładne identyfikatory są losowo zniekształcane.Wydaje się, że
A1
iA4
są najbardziej podobne. Dokładna kontrola wykaże, że rzeczywiście struktury kodu wA1
iA4
są identyczne. ToA2
iA3
podobne są również uzasadnione, ponieważ obie są zdefiniowane jako zestaw dwóch funkcji.Jeśli zamierzasz dokładnie zbadać dane
core
wyjściowe, sensowne jest także podanie flag takich jak-dsuppress-module-prefixes
i-dsuppress-uniques
. Ułatwiają czytanie.Krótka lista naszych wrogów.
Co może pójść nie tak z testami porównawczymi i optymalizacją?
ghci
, zaprojektowany do interaktywnej gry i szybkiej iteracji, kompiluje kod źródłowy Haskella do pewnego rodzaju kodu bajtowego, a nie końcowego pliku wykonywalnego, i unika kosztownych optymalizacji na rzecz szybszego przeładowania.To może wyglądać smutno. Ale tak naprawdę to nie powinno dotyczyć programisty Haskell, przez większość czasu. Prawdziwa historia: mam przyjaciela, który niedawno zaczął uczyć się Haskell. Napisali program do integracji numerycznej i był to żółw powolny. Usiedliśmy więc razem i napisaliśmy kategoryczny opis algorytmu wraz ze schematami i innymi rzeczami. Kiedy ponownie napisali kod, aby dostosować go do abstrakcyjnego opisu, magicznie stał się, jakby, gepardem szybkim i mało pamięci. Obliczyliśmy π w krótkim czasie. Morał historii? Idealna abstrakcyjna struktura, a Twój kod sam się zoptymalizuje.
źródło
ghci
wydaje się , że daje różne wyniki (pod względem prędkości) w porównaniu do tworzenia exe najpierw, jak powiedziałeś.