Продуктивність: рекурсія проти ітерації в JavaScript


24

Нещодавно я прочитав деякі статті (наприклад, http://dailyjs.com/2012/09/14/functional-programming/ ) про функціональні аспекти Javascript та взаємозв'язок між Scheme та Javascript (на останню вплинула перша, яка є функціональною мовою, в той час як аспекти ОО успадковуються від Self, який є мовою на основі прототипу).

Однак моє питання є більш конкретним: мені було цікаво, чи є показники про ефективність рекурсії проти ітерації в JavaScript.

Я знаю, що в деяких мовах (де ітерація дизайну працює краще) різниця мінімальна, оскільки інтерпретатор / компілятор перетворює рекурсію в ітерацію, проте, мабуть, це не стосується Javascript, оскільки це, принаймні частково, функціональний мова.


3
Зробіть власний тест і дізнайтеся одразу на jsperf.com
TehShrike

з щедротою та однією відповіддю, де згадується ТСО. Здається, що ES6 вказує TCO, але поки що ніхто не реалізує його, якщо ми вважаємо, що Kangax.github.io/compat-table/es6 Я щось пропускаю?
Маттіас Кауер

Відповіді:


28

JavaScript не здійснює оптимізацію хвостової рекурсії , тому якщо рекурсія занадто глибока, ви можете отримати переповнення стека викликів. Ітерації не мають таких проблем. Якщо ви думаєте, що ви будете повторювати занадто багато, і вам дійсно потрібна рекурсія (наприклад, щоб зробити заливку), замініть рекурсію на свій власний стек.

Продуктивність рекурсії, ймовірно, гірша, ніж продуктивність ітерації, оскільки виклики та повернення функцій вимагають збереження та відновлення стану, тоді як ітерація просто переходить до іншої точки функції.


Цікаво ... Я бачив трохи коду, де створюється порожній масив і сайт рекурсивної функції присвоюється позиції в масиві, а потім повертається значення, збережене в масиві. Це те, що ви маєте на увазі під «заміною рекурсії на свій власний стек»? Напр .: var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
мастазі

@mastazi: Ні, це зробить марний стек викликів разом із внутрішнім. Я мав на увазі щось на кшталт заповнення черг із Вікіпедії .
Triang3l

Варто зазначити, що мова не виконує TCO, але реалізація може бути. Те, як люди оптимізують JS, означає, що, можливо, TCO може з’явитися у кількох реалізаціях
Daniel Gratzer

1
@mastazi Замініть elseцю функцію на else if (stack[n-1]) { return stack[n-1]; } elseі у вас є запам'ятовування . Хто б не писав, що цей фактичний код мав неповну реалізацію (і, мабуть, він повинен був використовуватись stack[n]скрізь, а не stack[n-1]).
Ізката

Дякую @Izkata, я часто роблю таку оптимізацію, але до сьогоднішнього дня я не знав її назви. Я повинен був вивчати CS замість ІТ ;-)
mastazi

20

Оновлення : оскільки ES2015, JavaScript має TCO , тому частина наведеного нижче аргументу більше не стоїть.


Хоча у Javascript немає оптимізації хвостових викликів, рекурсія часто є найкращим способом. І щиро, за винятком крайових випадків, ви не збираєтесь отримувати переповнення стека викликів.

Продуктивність - це щось, про що слід пам’ятати, але і передчасна оптимізація. Якщо ви вважаєте, що рекурсія є більш елегантною, ніж ітерація, то займіться цим. Якщо виявиться, що це ваше вузьке місце (яке може ніколи не бути), то ви можете замінити якусь потворну ітерацію. Але в більшості випадків вузьке місце лежить в маніпуляціях DOM або, взагалі, вводу-виводу, а не самого коду.

Рекурсія завжди більш елегантна 1 .

1 : Особиста думка.


3
Я погоджуюсь, що рекурсія є більш елегантною, і важлива елегантність, оскільки вона є читабельністю, а також ремонтопридатністю (це суб'єктивно, але, на мою думку, рекурсію читати дуже легко, таким чином ремонтувати). Однак іноді питання має значення; чи можете ви підтримати твердження, що рекурсія - це найкращий спосіб, навіть з точки зору продуктивності?
мастазі

3
@mastazi, як було сказано у моїй відповіді, я сумніваюся, що рекурсія стане вашим вузьким місцем. У більшості випадків це маніпуляція DOM або, загалом, введення / виведення. І не забувайте, що передчасна оптимізація - корінь усіх злих;)
Флоріан Маргаїн

+1 за те, що маніпуляція з DOM в основному була вузьким місцем! Я пам’ятаю дуже цікаве інтерв'ю Єгуди Кац (Ember.js) з цього приводу.
mastazi

1
@mike Визначення поняття "недоношений" - "зрілий або дозрілий до належного часу". Якщо ви знаєте, що рекурсивно щось робити спричинить перебіг потоку, то це не передчасно. Однак якщо припустити примху (без фактичних даних), то це передчасно.
Зірак

2
З Javascript ви не маєте, скільки стеків буде доступна програма. У вас може бути крихітний стек в IE6 або великий стек у FireFox. Рекурсивні алгоритми рідко мають фіксовану глибину, якщо ви робите рекурсивний цикл у стилі схеми. Це просто не здається, ніби безрезультатна рекурсія підходить для уникнення передчасних оптимізацій.
mike30

7

Мені було дуже цікаво про цю ефективність і в javascript, тому я зробив кілька експериментів (хоча і на більш старій версії вузла). Я написав факторний калькулятор рекурсивно проти ітерацій і запустив його кілька разів локально. Результат видався досить сильно нахиленим до рекурсії, яка має податок (очікуваний).

Код: https://github.com/j03m/trickyQuestions/blob/master/factorial.js

Result:
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:557
Time:126
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:519
Time:120
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:541
Time:123
j03m-MacBook-Air:trickyQuestions j03m$ node --version
v0.8.22

Ви можете спробувати це "use strict";і побачити, чи це має значення. (Це створить jumps замість стандартної послідовності викликів)
Бердок

1
На останній версії вузла (6.9.1) я отримав надзвичайно схожі результати. Існує трохи податку на рекурсію, але я вважаю це кращим випадком - різниця 400 мс для 1000000 циклів становить .0025 мс за цикл. Якщо ви робите 1 000 000 циклів, це щось пам’ятати.
Келц

6

Відповідно до запиту ОП, я зіткнуся (не зроблячи дурня, сподіваюся: P)

Я думаю, що ми всі погодилися, що рекурсія - це просто більш елегантний спосіб кодування. Якщо все зроблено добре, це може зробити більш реконструйованим кодом, що IMHO так само важливо (якщо не більше), що гоління 0,0001ms.

Що стосується аргументу, що JS не проводить оптимізацію Tail-call: це вже не зовсім вірно, використання суворого режиму ECMA5 дозволяє TCO. Це було те, що я не надто тішився деякий час назад, але, принаймні, тепер я знаю, чому arguments.calleeкидає помилки в суворому режимі. Я знаю посилання вище посилання на звіт про помилку, але помилка встановлена ​​на WONTFIX. Крім того, стандартний TCO виходить: ECMA6 (грудень 2013).

Інстинктивно, і дотримуючись функціональної природи JS, я б сказав, що рекурсія - це більш ефективний стиль кодування 99,99% часу. Однак у Флоріана Маргаїна є момент, коли він каже, що вузьке місце, ймовірно, знайдеться в іншому місці. Якщо ви маніпулюєте DOM, ви, мабуть, найкраще зосередитесь на тому, щоб написати свій код якомога більш доступним. API DOM - це те, що це: повільно.

Я думаю, що неможливо запропонувати остаточну відповідь щодо швидшого варіанту. Останнім часом багато jspref, які я бачив, показують, що V8 двигун Chrome смішно швидкий при деяких завданнях, які працюють на 4 рази повільніше на SpiderMonkey FF і навпаки. Сучасні двигуни JS мають різноманітні хитрощі, щоб оптимізувати ваш код. Я не експерт, але я вважаю, що, наприклад, V8 дуже оптимізований для закриття (та рекурсії), тоді як JScript двигуна MS не є. SpiderMonkey часто працює краще, коли DOM залучається ...

Коротше кажучи: я б сказав, яка техніка буде більш ефективною, як завжди в JS, майже неможливо передбачити.


3

Без суворого режиму продуктивність ітерації, як правило, трохи швидша, ніж рекурсія ( на додаток до того, щоб JIT зробив більше роботи ). Оптимізація рекурсії хвоста по суті усуває будь-яку помітну різницю, оскільки перетворює всю послідовність викликів у стрибок.

Приклад: Jsperf

Я б запропонував турбуватися набагато більше про чіткість та простоту коду, коли мова йде про вибір між рекурсією та ітерацією.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.