Nurkuję w świecie programowania funkcjonalnego i czytam wszędzie, że języki funkcjonalne są lepsze dla programów wielowątkowych / wielordzeniowych. Rozumiem, w jaki sposób języki funkcjonalne robią wiele rzeczy inaczej, na przykład rekurencję , liczby losowe itp., Ale nie wydaje mi się, aby dowiedzieć się, czy wielowątkowość jest szybsza w języku funkcjonalnym, ponieważ jest inaczej skompilowana lub dlatego, że piszę inaczej.
Na przykład napisałem program w Javie, który implementuje pewien protokół. W tym protokole obie strony wysyłają i odbierają sobie tysiące wiadomości, szyfrują je i ponownie wysyłają (i odbierają). Zgodnie z oczekiwaniami wielowątkowość jest kluczowa, gdy masz do czynienia w skali tysięcy. W tym programie nie występuje blokowanie .
Jeśli napiszę ten sam program w Scali (który używa JVM), czy ta implementacja będzie szybsza? Jeśli tak, dlaczego? Czy to z powodu stylu pisania? Jeśli dzieje się tak ze względu na styl pisania, ponieważ teraz Java zawiera wyrażenia lambda, czy nie mógłbym osiągnąć takich samych wyników przy użyciu Java z lambda? A może jest to szybsze, ponieważ Scala będzie kompilować wszystko inaczej?
źródło
Odpowiedzi:
Powodem, dla którego ludzie mówią, że języki funkcjonalne są lepsze do przetwarzania równoległego, jest fakt, że zwykle unikają one stanu zmiennego. Zmienny stan jest „źródłem wszelkiego zła” w kontekście przetwarzania równoległego; sprawiają, że bardzo łatwo jest biegać w warunkach wyścigowych, gdy są one dzielone między równoległymi procesami. Rozwiązanie warunków wyścigu obejmuje następnie mechanizmy blokowania i synchronizowania, jak wspomniano, które powodują obciążenie w czasie wykonywania, ponieważ procesy czekają, aż nawzajem skorzystają z udostępnionego zasobu, i większa złożoność projektu, ponieważ wszystkie te koncepcje są zwykle głęboko zagnieżdżone w takich aplikacjach.
Gdy unikasz stanu zmiennego, wraz z nim znika potrzeba synchronizacji i mechanizmów blokowania. Ponieważ języki funkcjonalne zwykle unikają stanu zmiennego, są one naturalnie bardziej wydajne i wydajne dla przetwarzania równoległego - nie będziesz mieć narzutu zasobów współdzielonych i nie będziesz mieć dodatkowej złożoności projektowej, która zwykle następuje.
Jest to jednak przypadkowe. Jeśli Twoje rozwiązanie w Javie pozwala również na unikalny stan (szczególnie współdzielony między wątkami), konwersja do funkcjonalnego języka, takiego jak Scala lub Clojure, nie przyniosłaby żadnych korzyści pod względem równoczesnej wydajności, ponieważ oryginalne rozwiązanie jest już wolne od kosztów ogólnych spowodowanych przez mechanizmy blokujące i synchronizujące.
TL; DR: Jeśli rozwiązanie w Scali jest bardziej wydajne w przetwarzaniu równoległym niż jedno w Javie, nie dzieje się tak ze względu na sposób kompilacji lub uruchamiania kodu przez JVM, ale z tego powodu, że rozwiązanie Java dzieli stan zmiennych między wątkami, powodując warunki wyścigu lub dodając koszty synchronizacji, aby ich uniknąć.
źródło
W pewnym sensie oba. Jest szybszy, ponieważ łatwiej jest napisać kod w sposób łatwiejszy do szybszej kompilacji. Niekoniecznie uzyskasz różnicę prędkości, zmieniając języki, ale jeśli zacząłeś od języka funkcjonalnego, prawdopodobnie mógłbyś wykonać wielowątkowość przy znacznie mniejszym wysiłku programisty . W ten sam sposób programiście dużo łatwiej popełnia błędy w wątkach, które będą kosztować szybkość w języku imperatywnym, i znacznie trudniej je zauważyć.
Powodem jest to, że programiści zazwyczaj starają się umieścić cały bezblokowany, wątkowy kod w jak najmniejszym pudełku, i jak najszybciej uciec z niego, z powrotem do swojego wygodnego, synchronicznego, synchronicznego świata. Większość błędów, które kosztują Twoją prędkość, popełnianych jest na interfejsie granicznym. W funkcjonalnym języku programowania nie musisz się tak bardzo martwić popełnieniem błędów na tej granicy. Większość kodu telefonicznego znajduje się również „w pudełku”.
źródło
Programowanie funkcjonalne z reguły nie powoduje szybszych programów. Pozwala to na łatwiejsze programowanie równoległe i współbieżne. Są do tego dwa główne klucze:
Doskonałym przykładem punktu 2 jest to, że w Haskell mamy wyraźne rozróżnienie między równoległością deterministyczną a niedeterministyczną współbieżnością . Nie ma lepszego wytłumaczenia niż cytowanie doskonałej książki Simona Marlowa „ Programowanie równoległe i współbieżne” w Haskell (cytaty pochodzą z rozdziału 1 ):
Oprócz tego Marlow wspomina także o wymiarze determinizmu :
W Haskell funkcje równoległości i współbieżności zostały zaprojektowane wokół tych koncepcji. W szczególności, jakie inne języki grupują razem jako jeden zestaw funkcji, Haskell dzieli się na dwa:
Jeśli próbujesz tylko przyspieszyć czyste, deterministyczne obliczenia, posiadanie deterministycznego paralelizmu często znacznie ułatwia. Często po prostu robisz coś takiego:
Zrobiłem to w jednym z moich programów zabawkowych kilka tygodni temu . Równoważenie programu było trywialne - kluczową rzeczą, którą musiałem zrobić, było dodanie kodu, który mówi „oblicz równolegle elementy tej listy” (linia 90), i uzyskałem prawie liniową poprawę przepustowości w niektóre z moich droższych przypadków testowych.
Czy mój program jest szybszy niż gdybym korzystał z konwencjonalnych narzędzi do wielowątkowości opartych na blokadach? Bardzo w to wątpię. W moim przypadku fajną rzeczą było uzyskanie tak wielkiego huku z tak małej złotówki - mój kod jest prawdopodobnie bardzo nieoptymalny, ale ponieważ jest tak łatwy do zrównoleglenia, uzyskałem duże przyspieszenie przy znacznie mniejszym wysiłku niż odpowiednie profilowanie i optymalizacja, i bez ryzyka warunków wyścigu. I to, jak twierdzę, jest to główny sposób, w jaki programowanie funkcjonalne pozwala pisać „szybsze” programy.
źródło
W Haskell modyfikacja jest dosłownie niemożliwa bez uzyskania specjalnych modyfikowalnych zmiennych poprzez bibliotekę modyfikacji. Zamiast tego funkcje tworzą potrzebne zmienne w tym samym czasie, co ich wartości (które są obliczane leniwie), a śmieci są gromadzone, gdy nie są już potrzebne.
Nawet gdy potrzebujesz zmiennych do modyfikacji, zwykle możesz to zrobić, używając zapasowo i wraz ze zmiennymi niemodyfikowalnymi. (Kolejną fajną rzeczą w haskell jest STM, który zastępuje zamki operacjami atomowymi, ale nie jestem pewien, czy dotyczy to programowania funkcjonalnego, czy nie.) Zwykle tylko jedna część programu musi być połączona równolegle, aby poprawić rzeczy pod względem wydajności.
To sprawia, że równoległość w Haskell jest bardzo łatwa, a w rzeczywistości trwają starania, aby była ona automatyczna. W przypadku prostego kodu równoległość i logikę można nawet rozdzielić.
Ponadto ze względu na fakt, że kolejność oceny nie ma znaczenia w Haskell, kompilator po prostu tworzy kolejki rzeczy, które wymagają oceny, i wysyła je do dowolnych dostępnych rdzeni, dzięki czemu można utworzyć grupę „wątków”, które nie mają znaczenia faktycznie stają się wątkami, dopóki nie są konieczne. Kolejność oceny bez znaczenia jest charakterystyczna dla czystości, która zwykle wymaga programowania funkcjonalnego.
Dalsza lektura
Równoległość w Haskell (HaskellWiki)
Programowanie współbieżne i wielordzeniowe w „Real-World Haskell”
Programowanie równoległe i współbieżne w Haskell przez Simon Marlow
źródło
grep java this_post
.grep scala this_post
igrep jvm this_post
nie zwracają żadnych wyników :)