Zgadzam się z Dietrich Epp: to połączenie kilku rzeczy, które sprawiają, że GHC jest szybki.
Przede wszystkim Haskell ma bardzo wysoki poziom. Dzięki temu kompilator może przeprowadzać agresywne optymalizacje bez przerywania kodu.
Pomyśl o SQL. Teraz, kiedy piszę SELECT
oświadczenie, może to wyglądać na pętlę rozkazującą, ale tak nie jest . Może się wydawać, że zapętla się we wszystkich wierszach w tej tabeli, próbując znaleźć ten, który pasuje do określonych warunków, ale tak naprawdę „kompilator” (silnik DB) może zamiast tego wyszukiwać indeks - który ma zupełnie inną charakterystykę wydajności. Ponieważ jednak SQL jest tak wysoki, „kompilator” może zastępować całkowicie różne algorytmy, stosować wiele procesorów lub kanałów we / wy lub całe serwery w sposób transparentny i więcej.
Myślę, że Haskell jest taki sam. Może ci się wydawać , że poprosiłeś Haskella o zamapowanie listy danych wejściowych na drugą listę, przefiltrowanie drugiej listy do trzeciej listy, a następnie policz, ile wynikło wyników. Ale nie widziałeś, aby GHC stosowało za kulisami reguły przepisywania fuzji strumienia, przekształcając całość w pojedynczą ciasną pętlę kodu maszynowego, która wykonuje całe zadanie w jednym przejściu danych bez alokacji - coś, co być żmudnym, podatnym na błędy i niemożliwym do utrzymania pisaniem ręcznie. Jest to naprawdę możliwe tylko z powodu braku szczegółów niskiego poziomu w kodzie.
Innym sposobem spojrzenia na to może być… dlaczego Haskell nie miałby być szybki? Co to robi, że powinno spowolnić?
To nie jest interpretowany język, taki jak Perl czy JavaScript. To nawet nie jest system maszyn wirtualnych, takich jak Java czy C #. Kompiluje się aż do natywnego kodu maszynowego, więc nie ma narzutu.
W przeciwieństwie do języków OO [Java, C #, JavaScript…], Haskell ma pełny typ kasowania [jak C, C ++, Pascal…]. Wszystkie sprawdzanie typów odbywa się tylko w czasie kompilacji. Nie ma więc sprawdzania typu w czasie wykonywania, aby spowolnić. (W tym przypadku nie sprawdza się zerowych wskaźników. Na przykład w Javie JVM musi sprawdzić zerowe wskaźniki i zgłosić wyjątek, jeśli je uszanujesz. Haskell nie musi się tym przejmować).
Mówisz, że powolne jest „tworzenie funkcji w locie”, ale jeśli spojrzysz bardzo uważnie, tak naprawdę nie robisz tego. Może to wyglądać tak jak ty, ale tak nie jest. Jeśli powiesz (+5)
, cóż, jest to zakodowane na stałe w kodzie źródłowym. Nie można go zmienić w czasie wykonywania. Więc to nie jest tak naprawdę funkcja dynamiczna. Nawet funkcje curry naprawdę zapisują parametry w bloku danych. Cały kod wykonywalny faktycznie istnieje w czasie kompilacji; nie ma interpretacji w czasie wykonywania. (W przeciwieństwie do niektórych innych języków, które mają „funkcję ewaluacji”).
Pomyśl o Pascalu. Jest stary i nikt tak naprawdę go nie używa, ale nikt nie narzekałby, że Pascal jest wolny . Jest wiele rzeczy, których można nie lubić, ale powolność tak naprawdę nie jest jedną z nich. Haskell tak naprawdę nie robi tyle, co różni się od Pascala, poza zbieraniem pamięci zamiast ręcznego zarządzania pamięcią. Niezmienne dane pozwalają na kilka optymalizacji silnika GC [które leniwe oceny nieco komplikują].
Myślę, że chodzi o to, że Haskell wygląda na zaawansowanego, wyrafinowanego i na wysokim poziomie, i wszyscy myślą: „och, to jest naprawdę potężne, musi być niesamowicie wolne! ” Ale tak nie jest. A przynajmniej nie jest tak, jak można się spodziewać. Tak, ma niesamowity system pisania. Ale wiesz co? To wszystko dzieje się w czasie kompilacji. Z biegiem czasu już go nie ma. Tak, pozwala konstruować skomplikowane narzędzia ADT z wierszem kodu. Ale wiesz co? ADT tylko zwykły zwykły C union
na struct
sekundę. Nic więcej.
Prawdziwym zabójcą jest leniwa ocena. Gdy dobrze opanujesz ścisłość / lenistwo swojego kodu, możesz pisać głupio szybki kod, który jest nadal elegancki i piękny. Ale jeśli źle to zrobisz, twój program będzie tysiące razy wolniejszy i naprawdę nie jest oczywiste, dlaczego tak się dzieje.
Na przykład napisałem prosty, trywialny program do zliczania, ile razy każdy bajt pojawia się w pliku. W przypadku pliku wejściowego o wielkości 25 KB uruchomienie programu zajęło 20 minut i połknęło 6 gigabajtów pamięci RAM! To absurdalne !! Ale potem zdałem sobie sprawę, na czym polega problem, dodałem pojedynczy wzór huku, a czas pracy spadł do 0,02 sekundy .
Tutaj Haskell idzie nieoczekiwanie powoli. Przyzwyczajenie się do tego zajmuje trochę czasu. Ale z czasem łatwiej jest pisać naprawdę szybki kod.
Co sprawia, że Haskell jest tak szybki? Czystość. Typy statyczne. Lenistwo. Ale przede wszystkim będąc wystarczająco wysokim poziomem, aby kompilator mógł radykalnie zmienić implementację bez naruszania oczekiwań twojego kodu.
Ale myślę, że to tylko moja opinia ...