web-dev-qa-db-ja.com

効率:再帰とループ

これは私の側の好奇心ですが、より効率的な再帰またはループは何ですか?

与えられた2つの機能(一般的なLISPを使用):

(defun factorial_recursion (x)
    (if (> x 0)
        (* x (factorial_recursion (decf x)))
        1))

そして

(defun factorial_loop (x)
    (loop for i from 1 to x for result = 1 then
        (* result i) finally
        (return result)))

どちらがより効率的ですか?

16
SpaceFace

私はあなたのコードを読む必要さえありません。

階乗の場合、ループの方が効率的です。再帰を行う場合、スタックには最大xの関数呼び出しがあります。

パフォーマンス上の理由から、再帰を使用することはほとんどありません。問題をより単純にするために再帰を使用します。

ムー。

真剣に今、それは問題ではありません。たとえば、このサイズではありません。それらは両方とも同じ複雑さを持っています。コードの速度が十分でない場合、これはおそらく最後に見る場所の1つです。

ここで、どちらが速いかを本当に知りたい場合は、それらを測定します。 SBCLでは、ループ内の各関数を呼び出して時間を測定できます。 2つの単純な関数があるので、 time で十分です。プログラムがもっと複​​雑な場合は、 profiler の方が便利です。ヒント:測定にプロファイラーが必要ない場合は、おそらくパフォーマンスについて心配する必要はありません。

私のマシン(SBCL 64ビット)で、関数を実行してこれを取得しました:

CL-USER> (time (loop repeat 1000 do (factorial_recursion 1000)))
Evaluation took:
  0.540 seconds of real time
  0.536034 seconds of total run time (0.496031 user, 0.040003 system)
  [ Run times consist of 0.096 seconds GC time, and 0.441 seconds non-GC time. ]
  99.26% CPU
  1,006,632,438 processor cycles
  511,315,904 bytes consed

NIL
CL-USER> (time (loop repeat 1000 do (factorial_loop 1000)))
Evaluation took:
  0.485 seconds of real time
  0.488030 seconds of total run time (0.488030 user, 0.000000 system)
  [ Run times consist of 0.072 seconds GC time, and 0.417 seconds non-GC time. ]
  100.62% CPU
  902,043,247 processor cycles
  511,322,400 bytes consed

NIL

関数を(declaim (optimize speed))が先頭にあるファイルに配置した後、再帰時間は504ミリ秒に短縮され、ループ時間は475ミリ秒に短縮されました。

そして、本当に何が起こっているのかを知りたい場合は、関数で dissasemble を試して、そこに何があるかを確認してください。

繰り返しますが、これは私には問題ではないように見えます。個人的には、プロトタイピング用のスクリプト言語のようにCommon LISPを使用してから、遅い部分のプロファイルを作成して最適化しようとしています。 500msから475msに到達することは何もありません。たとえば、一部の個人コードでは、要素タイプを配列に追加するだけで数桁高速化されました(したがって、私の場合、配列ストレージは64分の1になります)。確かに、理論的には、その配列を(小さくした後)再利用し、何度も割り当てない方が速いでしょう。しかし、私の状況では、単に:element-type bitを追加するだけで十分でした。変更を増やすと、追加のメリットがほとんどないため、より多くの時間が必要になります。たぶん私はずさんですが、「速い」と「遅い」は私にはあまり意味がありません。私は「十分に速い」と「遅すぎる」を好みます。ほとんどの場合、両方の関数は「十分に高速」であるため(または、場合によっては両方が「遅すぎる」)、それらの間に実際の違いはありません。

12

再帰呼び出しが最後のこと完了するように再帰関数を記述できる場合(したがって、関数は末尾再帰および使用している言語とコンパイラ/インタプリタは末尾再帰をサポートしているため、再帰関数は(通常)実際に反復的で、同じ関数の反復バージョンと同じくらい高速なコードに最適化できます。

Sam I Amは正しいですが、反復関数は通常、再帰関数よりも高速です。再帰関数が同じことを行う反復関数と同じくらい高速である場合は、オプティマイザーに依存する必要があります。

この理由は、関数呼び出しがジャンプよりもはるかに高価であり、さらにスタックスペース((非常に)有限のリソース)を消費するためです。

factorial_recursionを呼び出してから、それをxで乗算するため、指定する関数は末尾再帰ではありません。末尾再帰バージョンの例は次のようになります。

(defun factorial-recursion-assist (x cur)
    (if (> x 1)
        (factorial-recursion-assist (- x 1) (+ cur (* (- x 1) x)))
        cur))

(defun factorial-recursion (x)
    (factorial-recursion-assist x 1))

(print (factorial-recursion 4))
9
Seth Carnegie