Производительность: рекурсия против итерации в Javascript

24

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

Однако мой вопрос более конкретен: мне было интересно, есть ли показатели производительности рекурсии и итерации в Javascript.

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

mastazi
источник
3
Сделайте свой собственный тест и узнайте прямо сейчас на jsperf.com
TehShrike 18.12.12
с наградой и одним ответом с упоминанием TCO. Похоже, что ES6 определяет TCO, но пока никто не реализует его, если мы считаем, что kangax.github.io/compat-table/es6 Я что-то упустил?
Матиас Кауэр

Ответы:

28

JavaScript не выполняет оптимизацию хвостовой рекурсии , поэтому, если ваша рекурсия слишком глубока, вы можете получить переполнение стека вызовов. У итерации таких проблем нет. Если вы думаете, что собираетесь использовать слишком много рекурсии, и вам действительно нужна рекурсия (например, для выполнения заливки), замените рекурсию собственным стеком.

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

Triang3l
источник
Просто интересно ... Я видел немного кода, где создается пустой массив и сайт рекурсивной функции назначается позиции в массиве, а затем возвращается значение, сохраненное в массиве. Это то, что вы подразумеваете под "заменить рекурсию своим собственным стеком"? Например: 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 может появиться в нескольких реализациях
Даниэль Гратцер,
1
@mastazi Замените elseв этой функции на, else if (stack[n-1]) { return stack[n-1]; } elseи у вас есть памятка . Кто бы ни написал этот факторный код, его реализация была неполной (и, вероятно, следовало бы использовать stack[n]везде, а не stack[n-1]).
Изката
Спасибо @Izkata, я часто занимаюсь такой оптимизацией, но до сегодняшнего дня я не знал ее названия. Я должен был изучать CS вместо IT ;-)
mastazi
20

Обновление : начиная с ES2015, JavaScript имеет TCO , поэтому часть приведенного ниже аргумента больше не действует.


Хотя в Javascript нет оптимизации хвостовых вызовов, рекурсия часто является лучшим способом. И искренне, за исключением крайних случаев, вы не получите переполнения стека вызовов.

Производительность - это то, что нужно учитывать, но преждевременная оптимизация тоже. Если вы думаете, что рекурсия более элегантна, чем итерация, то сделайте это. Если окажется, что это ваше узкое место (которое может никогда не быть), то вы можете заменить его на некрасивую итерацию. Но в большинстве случаев узким местом является манипулирование DOM или, в более общем смысле, ввод-вывод, а не сам код.

Рекурсия всегда более элегантна 1 .

1 : Личное мнение.

Флориан Маргейн
источник
3
Я согласен с тем, что рекурсия более элегантна, и элегантность важна, так как она удобочитаема и удобна в обслуживании (это субъективно, но, на мой взгляд, рекурсия очень легко читается и, следовательно, поддерживается). Однако иногда производительность имеет значение; Можете ли вы поддержать утверждение, что рекурсия - лучший путь, даже с точки зрения производительности?
Мастази 18.12.12
3
@mastazi, как сказал в моем ответе, я сомневаюсь, что рекурсия будет вашим узким местом. В большинстве случаев это манипуляции с DOM или, в более общем смысле, ввод-вывод. И не забывайте, что преждевременная оптимизация - корень всех зол;)
Florian Margaine
+1 за манипуляцию с DOM в большинстве случаев является узким местом! Я помню очень интересное интервью с Иегудой Кацем (Ember.js) по этому поводу.
Мастази
1
@mike Определение термина «преждевременный» - «зрелый или зрелый раньше времени». Если вы знаете, что рекурсивное выполнение чего-либо вызовет переполнение стека, то это не преждевременно. Однако, если вы предполагаете по прихоти (без каких-либо реальных данных), то это преждевременно.
Зирак
2
С Javascript вы не знаете, сколько стеков будет доступно программе. Вы можете иметь крошечный стек в IE6 или большой стек в FireFox. Рекурсивные алгоритмы редко имеют фиксированную глубину, если вы не делаете рекурсивный цикл в стиле Scheme. Просто не похоже, что рекурсия, не основанная на циклах, не позволяет избежать преждевременной оптимизации.
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
Dr.HappyPants
источник
Вы можете попробовать это с "use strict";и посмотреть, если это имеет значение. (Он выдаст jumps вместо стандартной последовательности вызовов)
Burdock
1
На последней версии узла (6.9.1) я получил очень похожие результаты. Существует реальный налог на рекурсию, но я считаю это крайним случаем - разница 400 мс для 1000000 циклов составляет 0,0025 мс на цикл. Если вы делаете 1 000 000 циклов, это стоит помнить.
Кельц
6

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

Я думаю, что мы все согласны с тем, что рекурсия - это просто более элегантный способ кодирования. Если все сделано правильно, это может сделать код более легким для сопровождения, что, по-моему, так же важно (если не больше), как снижение затрат на 0,0001 мс.

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

Инстинктивно, придерживаясь функциональной природы JS, я бы сказал, что рекурсия - более эффективный стиль кодирования в 99,99% случаев. Тем не менее, у Флориана Маргэйна есть пункт, когда он говорит, что узкое место может быть найдено в другом месте. Если вы манипулируете DOM, вам, вероятно, лучше всего сосредоточиться на написании своего кода, насколько это возможно. DOM API - это то, что он есть: медленно.

Я думаю, что невозможно дать однозначный ответ, какой вариант является более быстрым. В последнее время многие jspref, которые я видел, показывают, что движок Chrome V8 смехотворно быстр в некоторых задачах, которые работают в 4 раза медленнее на Fider SpiderMonkey и наоборот. Современные движки JS имеют всевозможные хитрости для оптимизации вашего кода. Я не эксперт, но я чувствую, что V8, например, высоко оптимизирован для замыканий (и рекурсии), тогда как движок MS JScript - нет. SpiderMonkey часто работает лучше, когда DOM задействован ...

Вкратце: я бы сказал, какая техника будет более эффективной, как всегда в JS, почти невозможно предсказать.

Элиас Ван Оотегем
источник
3

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

Пример: Jsperf

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

лопух
источник