パフォーマンス:Javascriptでの再帰と反復


24

私は最近、Javascriptの機能面と、SchemeとJavascriptの関係に関する後者の記事(例:http : //dailyjs.com/2012/09/14/functional-programming/)を読みました。 OOの側面はプロトタイピングベースの言語であるSelfから継承されますが、機能的な言語です。

しかし、私の質問はより具体的です:Javascriptの再帰と反復のパフォーマンスに関するメトリックがあるかどうか疑問に思っていました。

一部の言語では(設計の反復によりパフォーマンスが向上する)、インタープリター/コンパイラーが再帰を反復に変換するため、差は最小限に抑えられますが、少なくとも部分的には機能的であるため、おそらくJavascriptのケースではないでしょう言語。


3
独自のテストを行い、すぐにjsperf.com
TehShrike

報奨金とTCOに言及する1つの答えがあります。ES6はTCOを指定しているように見えますが、kangax.github.io / compat-table / es6を信じている場合、これまでのところ誰もTCOを実装していません。
マティアスカウアー

回答:


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

@mastazi:いいえ、これは内部の呼び出しスタックと一緒に無駄な呼び出しスタックを作成します。Wikipediaのキューベースのフラッドフィルのようなものを意味しました。
Triang3l

言語はTCOを実行しませんが、実装は実行する可能性があることに注意してください。人々がJSを最適化する方法は、おそらくTCOがいくつかの実装に現れる可能性があることを意味します
ダニエルグラッツァー

1
@mastazi elseその関数のをに置き換えると、メモ化がelse if (stack[n-1]) { return stack[n-1]; } elseできます。階乗コードの実装が不完全であることを書いた人は誰でも(おそらくの代わりにどこでも使用すべきだった)。stack[n]stack[n-1]
イズカタ

@Izkataに感謝します。私はそのような最適化を頻繁に行いますが、今日までその名前を知りませんでした。ITの代わりにCSを勉強すべきだった;-)
mastazi

20

更新:ES2015以降、JavaScriptにはTCOがあるため、以下の議論の一部はもう成り立ちません。


Javascriptには末尾呼び出しの最適化はありませんが、多くの場合、再帰が最善の方法です。そして、誠実に、エッジの場合を除いて、呼び出しスタックのオーバーフローは発生しません。

パフォーマンスは心に留めておくべきことですが、最適化も時期尚早です。再帰が反復よりもエレガントだと思うなら、それを選んでください。これがあなたのボトルネックであることが判明した場合(これは決してないかもしれません)、いくつかのsomeいイテレーションに置き換えることができます。ただし、ほとんどの場合、ボトルネックはコード自体ではなく、DOM操作またはより一般的にはI / Oにあります。

再帰は常によりエレガントです1

1:個人的な意見。


3
再帰はよりエレガントで、読みやすさと保守性のために優雅さが重要であることに同意します(これは主観的ですが、私の意見では、再帰は非常に読みやすく、したがって保守可能です)。ただし、パフォーマンスが重要になる場合があります。パフォーマンスに関しても再帰が最善の方法であるという主張を支持できますか?
マスタジ

3
私の答えで言ったように、@ mastaziは、再帰がボトルネックになるとは思いません。ほとんどの場合、DOM操作、またはより一般的にはI / Oです。時期尚早な最適化がすべての悪の根源であることを忘れないでください;)
フロリアンマーゲイン

ほとんどの場合、DOM操作がボトルネックになるため+1です。これについて、Yehuda Katz(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

あなたはこれを試して、"use strict";それが違いを生むかどうかを見ることができます。(jump標準の呼び出しシーケンスの代わりにs を生成します)
Burdock

1
最近のバージョンのノード(6.9.1)では、非常に似た結果が得られました。再帰には少し負担がかかりますが、私はそれをエッジケースと考えています-1,000,000ループの400msの差は、ループごとに.0025 msです。1,000,000ループを実行している場合は、留意する必要があります。
ケルツ

6

あたりとしてOPの要求私は(うまくいけば、自分の馬鹿を加えることなく:P)におけるチップよ

再帰はより洗練されたコーディング方法であるという点で、私たち全員が同意していると思います。うまく行けば、保守性の高いコードを作成できます。これは、0.0001msを削減することと同じくらい重要です(それ以上ではないにしても)。

JSがTail-call最適化を実行しないという議論に関する限り、それは完全に真実ではありません。ECMA5の厳格モードを使用するとTCOが有効になります。しばらく前にはあまり満足していなかったが、少なくとも今arguments.calleeは厳密モードでエラーをスローする理由を知っている。上記のリンクはバグレポートにリンクしていますが、バグはWONTFIXに設定されています。さらに、標準TCOが近づいています:ECMA6(2013年12月)。

本能的に、そしてJSの機能的性質にこだわり、再帰は99.99%の時間でより効率的なコーディングスタイルだと思います。ただし、Florian Margaine氏は、ボトルネックは他の場所で見つかる可能性が高いと述べています。DOMを操作している場合は、可能な限り保守可能なコードの記述に集中するのが最適です。DOM APIとは、遅いものです。

私はどちらがより速い選択肢であるかについて決定的な答えを提供することは不可能に近いと思います。最近、jsprefの多くは、私はChromeのV8エンジンがあることを示して見てきたのです途方もなくその逆FFのSpiderMonkeyのと副に遅くなる4倍を実行するいくつかのタスク、で速いし。最新のJSエンジンには、コードを最適化するためのあらゆる種類のトリックがあります。私は専門家ではありませんが、たとえばV8はクロージャー(および再帰)に対して高度に最適化されていますが、MSのJScriptエンジンはそうではありません。SpiderMonkeyは、DOMが関係している場合にパフォーマンスが向上することがよくあります...

要するに、JSでいつもそうであるように、どの手法がよりパフォーマンスが良いかは、予測するのが不可能に近いと言えます。


3

厳密モードを使用しない場合、反復パフォーマンスは通常、再帰よりもわずかに高速です(さらに、JITにより多くの作業を行わせることに加えて)。末尾再帰の最適化は、呼び出しシーケンス全体をジャンプに変えるため、本質的に顕著な違いを排除します。

例:Jsperf

再帰と反復のどちらを選択するかについては、コードの明快さと単純さについてさらに心配することをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.