私はCommon Lisp関数(私はまだ初心者です)のパフォーマンスを理解する上で問題があります。私はこの関数の2つのバージョンを持っています。これは与えられた最大の整数の合計を単に計算します。n
Common Lisp:テール再帰関数がスタックオーバーフローを引き起こすのはなぜですか?
非末尾再帰バージョン:
(defun addup3 (n)
(if (= n 0)
0
(+ n (addup (- n 1)))))
末尾再帰バージョン:
(defun addup2 (n)
(labels ((f (acc k)
(if (= k 0)
acc
(f (+ acc k) (- k 1)))))
(f 0 n)))
私は入力n = 1000000
とCLISPでこれらの関数を実行しようとしています。ここで結果
[2]> (addup3 1000000)
500000500000
[3]> (addup2 1000000)
*** - Program stack overflow. RESET
私はSBCLの両方で正常に実行することができますが、非末尾再帰一つは高速です(少しだけによって、それは私には奇妙に思えます)。私は答えのためのStackoverflowの質問を洗ったが、何か似たものを見つけることができませんでした。テール再帰関数はすべての再帰関数呼び出しをスタックに入れないように設計されていますが、なぜスタックオーバーフローが発生しますか?テールコールを最適化するためにインタプリタ/コンパイラに指示する必要がありますか? (私は(proclaim '(optimize (debug 1))
のようなものを読んで、デバッグレベルを設定し、トレース能力を犠牲にして最適化しましたが、私はこれが何であるか分かりません)。 多分答えは明らかでコードは間違っていますが、わかりません。 ヘルプは高く評価されています。
編集:danleiはタイプミスを指摘し、最初の関数でaddup3
を呼び出す必要があります。再帰的です。場合は修正し、両方のバージョンのオーバーフローではなく、彼の1
(defun addup (n)
"Adds up the first N integers"
(do ((i 0 (+ i 1))
(sum 0 (+ sum i)))
((> i n) sum)))
それはそれを行うには、より一般的な方法かもしれないが、私は私のインストラクターが、それはだを教えしたいと考えると、末尾再帰が常に最適化されていないこと、それは奇妙な見つけますはるかに効率的で物事。
danleiによって指摘されているように、最初の関数はtypoを持ち、 'addup'ではなく、自分自身を呼び出す必要があります。タイプミスを訂正すると、それもオーバーフローします。それでも、私はdo構造が再帰構造よりも優れていることに困惑しています。 clisp.orgの仕様をブラウジングしたりブラウズしているときに、CLISPのTCOに関する何かを見つけることはできません。尾の再帰が普通の再帰より強力でないならば、それは奇妙ではないでしょうか? – oarfish
あなたは驚かないでください。テールコールの最適化は、反復的な定義と同じくらい速くなるように、定数(スタック)空間で再帰的な定義を実行するだけです。それよりも速くできる魔法はありません。 – Svante
Barski's Land of Lispでは、関数をコンパイルするときに実際にCLISPがテールコールを最適化するだけであることを読んでいます。実際、尾の再帰的なものは少し速いので、ここでは謎はありません。 – oarfish