Niedawno przeczytałem kilka artykułów (np. Http://dailyjs.com/2012/09/14/functional-programming/ ) na temat funkcjonalnych aspektów Javascript i relacji między Scheme i Javascript (na ten drugi miał wpływ pierwszy, na który jest językiem funkcjonalnym, podczas gdy aspekty OO są dziedziczone z Self, który jest językiem opartym na prototypowaniu).
Moje pytanie jest jednak bardziej szczegółowe: zastanawiałem się, czy istnieją wskaźniki dotyczące wydajności rekurencji w porównaniu z iteracją w JavaScript.
Wiem, że w niektórych językach (gdzie z założenia iteracja działa lepiej) różnica jest minimalna, ponieważ interpreter / kompilator konwertuje rekurencję na iterację, jednak sądzę, że prawdopodobnie nie jest tak w przypadku Javascript, ponieważ jest on przynajmniej częściowo funkcjonalny język.
Odpowiedzi:
JavaScript nie wykonuje optymalizacji rekurencji ogona , więc jeśli twoja rekurencja jest zbyt głęboka, możesz dostać przepełnienie stosu wywołań. Iteracja nie ma takich problemów. Jeśli uważasz, że masz zamiar za dużo powtórzyć i naprawdę potrzebujesz rekurencji (na przykład, aby wypełnić zalewanie), zamień rekursję na własny stos.
Wydajność rekurencji jest prawdopodobnie gorsza niż wydajność iteracji, ponieważ wywołania funkcji i powroty wymagają zachowania i przywrócenia stanu, podczas gdy iteracja po prostu przeskakuje do innego punktu funkcji.
źródło
var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
else
w tej funkcji naelse if (stack[n-1]) { return stack[n-1]; } else
i masz notatkę . Ktokolwiek napisał ten kod czynnikowy miał niepełną implementację (i prawdopodobnie powinien był użyćstack[n]
wszędzie zamiaststack[n-1]
).Aktualizacja : od ES2015 JavaScript ma całkowity koszt posiadania , więc część poniższego argumentu już nie ma znaczenia.
Mimo że JavaScript nie ma optymalizacji wywołania ogona, rekursja jest często najlepszym sposobem. I szczerze, z wyjątkiem przypadków skrajnych, nie dostaniesz przepełnienia stosu wywołań.
Należy pamiętać o wydajności, ale także o przedwczesnej optymalizacji. Jeśli uważasz, że rekurencja jest bardziej elegancka niż iteracja, wybierz ją. Jeśli okaże się, że to twoje wąskie gardło (które może nigdy nie być), możesz zastąpić je brzydką iteracją. Ale w większości przypadków wąskim gardłem są manipulacje DOM lub bardziej ogólnie We / Wy, a nie sam kod.
Rekurencja jest zawsze bardziej elegancka 1 .
1 : Osobista opinia.
źródło
Byłem bardzo ciekawy tej wydajności w javascript, więc przeprowadziłem kilka eksperymentów (aczkolwiek na starszej wersji węzła). Napisałem kalkulator czynnikowy rekurencyjnie w stosunku do iteracji i uruchomiłem go kilka razy lokalnie. Wynik wydawał się dość mocno wypaczony w kierunku rekurencji z podatkiem (oczekiwany).
Kod: https://github.com/j03m/trickyQuestions/blob/master/factorial.js
źródło
"use strict";
i sprawdzić, czy to robi różnicę. (Wyprodukujejump
s zamiast standardowej sekwencji wywołań)Zgodnie z prośbą OP włożę się (bez robienia z siebie głupca, mam nadzieję: P)
Myślę, że wszyscy się zgodziliśmy, że rekurencja jest po prostu bardziej eleganckim sposobem kodowania. Jeśli zostanie to zrobione dobrze, może sprawić, że kod będzie łatwiejszy w utrzymaniu, co jest równie ważne (jeśli nie więcej) IMHO, że goli 0,0001 ms.
Jeśli chodzi o argument, że JS nie wykonuje optymalizacji wywołania ogona: nie jest to już do końca prawdą, użycie trybu ścisłego ECMA5 umożliwia TCO. To było coś, z czego nie byłem zbyt zadowolony, ale przynajmniej teraz wiem, dlaczego
arguments.callee
rzuca błędy w trybie ścisłym. Wiem, że powyższy link prowadzi do raportu o błędzie, ale błąd jest ustawiony na WONTFIX. Ponadto nadchodzi standardowy TCO: ECMA6 (grudzień 2013 r.).Instynktownie i trzymając się funkcjonalnej natury JS, powiedziałbym, że rekurencja jest bardziej wydajnym stylem kodowania 99,99% czasu. Jednak Florian Margaine ma rację, gdy mówi, że wąskie gardło można znaleźć gdzie indziej. Jeśli manipulujesz DOM, prawdopodobnie najlepiej skupiasz się na pisaniu kodu tak, aby był możliwie jak najłatwiejszy w utrzymaniu. DOM API jest tym, czym jest: wolno.
Myślę, że prawie niemożliwe jest udzielenie ostatecznej odpowiedzi na pytanie, która opcja jest szybsza. Ostatnio wiele jsprefów, które widziałem, pokazuje, że silnik Chrome V8 jest absurdalnie szybki w niektórych zadaniach, które działają 4x wolniej na SpiderMonkey w FF i odwrotnie. Nowoczesne silniki JS mają różne sztuczki, aby zoptymalizować kod. Nie jestem ekspertem, ale wydaje mi się, że na przykład V8 jest wysoce zoptymalizowany do zamykania (i rekurencji), podczas gdy silnik JScript MS nie. SpiderMonkey często działa lepiej, gdy DOM jest zaangażowany ...
W skrócie: Powiedziałbym, która technika będzie bardziej wydajna, jak zwykle w JS, prawie niemożliwe do przewidzenia.
źródło
Bez trybu ścisłego wydajność iteracji jest zwykle nieco szybsza niż rekurencja ( oprócz zwiększania wydajności JIT ). Optymalizacja rekurencji ogona zasadniczo eliminuje zauważalną różnicę, ponieważ zmienia całą sekwencję wywołań w skok.
Przykład: Jsperf
Sugerowałbym martwienie się o przejrzystość i prostotę kodu, jeśli chodzi o wybór między rekurencją a iteracją.
źródło