2013-05-18 9 views
7

私はCommon Lisp関数(私はまだ初心者です)のパフォーマンスを理解する上で問題があります。私はこの関数の2つのバージョンを持っています。これは与えられた最大の整数の合計を単に計算します。nCommon 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))) 

それはそれを行うには、より一般的な方法かもしれないが、私は私のインストラクターが、それはだを教えしたいと考えると、末尾再帰が常に最適化されていないこと、それは奇妙な見つけますはるかに効率的で物事。

答えて

7

Common Lispの実装でテールコールの最適化を行う必要はありません。しかし、ほとんどの場合、(私は、ABCLはJava仮想マシンの限界のためにそうではないと思う)。

実装のドキュメントでは、TCO(使用可能な場合)を得るために最適化設定を選択する必要があります。もちろん、「少しガウス」を使用することをお勧めします。この場合、

(loop :for i :upto n 
     :sum i) 

(let ((sum 0)) 
    (dotimes (i n) 
    (incf sum (1+ i)))) 

(do ((i 0 (1+ i)) 
    (sum 0 (+ sum i))) 
    ((> i n) sum)) 

ループ構造のいずれかを使用するCommon Lispのコードのためのより多くの慣用的である

(/ (* n (1+ n)) 2) 
+1

danleiによって指摘されているように、最初の関数はtypoを持ち、 'addup'ではなく、自分自身を呼び出す必要があります。タイプミスを訂正すると、それもオーバーフローします。それでも、私はdo構造が再帰構造よりも優れていることに困惑しています。 clisp.orgの仕様をブラウジングしたりブラウズしているときに、CLISPのTCOに関する何かを見つけることはできません。尾の再帰が普通の再帰より強力でないならば、それは奇妙ではないでしょうか? – oarfish

+0

あなたは驚かないでください。テールコールの最適化は、反復的な定義と同じくらい速くなるように、定数(スタック)空間で再帰的な定義を実行するだけです。それよりも速くできる魔法はありません。 – Svante

+0

Barski's Land of Lispでは、関数をコンパイルするときに実際にCLISPがテールコールを最適化するだけであることを読んでいます。実際、尾の再帰的なものは少し速いので、ここでは謎はありません。 – oarfish

4

あなたのaddup3はちょうど再帰的ではありません全くです。

(defun addup3 (n) 
    (if (= n 0) 
    0 
    (+ n (addup (- n 1))))) ; <-- 

addupのいずれかを呼び出します。 SBCLで修正されたバージョンを試す:

CL-USER> (defun addup3 (n) 
      (if (= n 0) 
       0 
       (+ n (addup3 (- n 1))))) 
ADDUP3 
CL-USER> (addup3 100000) 
Control stack guard page temporarily disabled: proceed with caution 
; .. 
; Evaluation aborted on #<SB-SYS:MEMORY-FAULT-ERROR {C2F19B1}>. 

+0

他のすべては、Svanteによって正しく評価されています。 – danlei

+0

オハイオ州の神、どのように愚か。私はこれらのタイプの誤植を検出するのに本当に悪いです。ありがとう。上記に含まれていなかった 'addup'関数も同じですが、' do'構造体を持ちます。それは、しかし、呼び出すことを意味しませんでした。 – oarfish

+1

心配する必要はありません。私たちはすべてこのようなときどき間違いを犯します。彼らは見つけにくいです。 – danlei

1

GNU CommonLisp、GCL 2.6を使用しています。12、コンパイルaddup2は、テールコールを最適化します。ここに私が持っているものがあります:

>(compile 'addup2)                  

Compiling /tmp/gazonk_3012_0.lsp. 
End of Pass 1. 

;; Note: Tail-recursive call of F was replaced by iteration. 
End of Pass 2. 
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3 
Finished compiling /tmp/gazonk_3012_0.lsp. 
Loading /tmp/gazonk_3012_0.o 
start address -T 0x9556e8 Finished loading /tmp/gazonk_3012_0.o 
#<compiled-function ADDUP2> 
NIL 
NIL 

>>(addup2 1000000)                                    

500000500000 
>(addup3 1000000) 

Error: ERROR "Invocation history stack overflow." 
Fast links are on: do (si::use-fast-links nil) for debugging 
Signalled by IF. 
ERROR "Invocation history stack overflow." 

Broken at +. Type :H for Help. 
    1 Return to top level. 

>>(compile 'addup3)                                   

Compiling /tmp/gazonk_3012_0.lsp. 
End of Pass 1. 
End of Pass 2. 
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3 
Finished compiling /tmp/gazonk_3012_0.lsp. 
Loading /tmp/gazonk_3012_0.o 
start address -T 0x955a00 Finished loading /tmp/gazonk_3012_0.o 
#<compiled-function ADDUP3> 
NIL 
NIL 
>>(addup3 1000000)                                    

Error: ERROR "Value stack overflow." 

希望します。

関連する問題