LoginSignup
8
3

More than 5 years have passed since last update.

Haskellでは末尾再帰にする必要が無いのかという疑問 #Haskell

Last updated at Posted at 2017-05-17

昨日の記事に対してarowMさんから

実はHaskellの場合は遅延評価の影響で、末尾再帰にしても

fact' x y = fact' (x - 1) (x * y)
(x - 1) とか (x * y) とかが評価されずに残り続けてしまうので、あまりうまみがなかったりっていう話もあります。
http://d.hatena.ne.jp/kazu-yamamoto/20091122/1258899591

もしかしたら最近のGHCの状況では話は別かもしれないです。
そのへんはきっと誰かが教えてくれるはず。

というコメントがありました(arowMさん、ありがとうございました).

僕もこの記事を読んだ事がありましたが, 通常の再帰と末尾再帰で圧倒的に体感時間の差があったため, もうこの事を忘れて勢い余って投稿してしまいました.

もしかしたら末尾再帰を理解した事が嬉しすぎて時間が経つのが早く感じてしまったのじゃないかと思ったので, ちゃんと検証していきたいと思います.

検証開始

と言っても大それた事をするわけではありません. ただプログラムの実行時間を計るだけです.
測定に使うのはMacのtimeコマンドです.
引数にコマンドや実行ファイルを与えると, そのコマンドを実行して実行にかかった時間を教えてくれます.

ソースコード

では次に比較に使うプログラムです.

通常再帰

fib1.hs
fib :: Integer -> Integer
fib 0 = 1
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

main = print $ fib 35

末尾再帰

fib2.hs
fib' :: Integer -> Integer
fib' n = fib'' n 1 1
    where
        fib'' n x y
            | n == 0 = 1
            | n == 1 = x
            | otherwise = fib'' (n - 1) (x + y) (x)

main = print $ fib' 35

はい, 例によってまたフィボナッチ数列です.
好きなんですよ, フィボナッチ数列.

測定

ではこれから測定をしていきたいと思います.
測定に使ったマシンはMacBook Pro (Retina, 15-inch, Mid 2014)で, CPUは2.2 GHz Intel Core i7です.

まずは1回目

実行結果
time ./fib1      
14930352
./fib1  1.81s user 0.02s system 98% cpu 1.845 total

time ./fib2      
14930352
./fib2  0.00s user 0.01s system 45% cpu 0.016 total

......圧倒的ですね

もう5回ずつやった結果の表が以下の通りです.

測定結果

回数 通常再帰(sec) 末尾再帰(sec)
1 1.845 0.016
2 1.910 0.015
3 1.893 0.008
4 1.842 0.009
5 1.870 0.008
6 1.892 0.008
平均 1.8753 0.0107

170倍以上の差が出ていますね.
正直ここまで差が出るとは思いませんでした.

ちなみにCPU使用率の平均は通常再帰が98.67%, 末尾再帰が57.33%でした.

結論

最新のGHCではどうやら末尾再帰最適化(?)が行われているっぽいですね.
もう少し詳しく調べてみようと思います.

追記

SaitoAtsushiさんから

これはそもそも同じ計算量ではないです。 式変形の段階でアルゴリズムが変わっています。 以前に似たような事例を見て解説記事を書いたことがあるので参考にしてください。

フィボナッチ関数の計算量 - 主題のない日記

という指摘をいただきました.
実際に実行過程を追っていきたいと思います.
(申し訳ありません, 通常再帰は左側優先, 末尾再帰は右側優先で計算してしまいました. 計算結果と計算量に差はありませんので, ご了承下さい)

通常再帰実行過程
fib 5

(fib 4) + (fib 3)

(fib 4) + ((fib 2) + (fib 1))

(fib 4) + ((fib 2) + 1)

(fib 4) + (((fib 1) + (fib 0)) + 1) 

(fib 4) + (((fib 1) + 1) + 1)

(fib 4) + ((1 + 1) + 1)

(fib 4) + (2 + 1)

(fib 4) + 3

((fib 3) + (fib 2)) + 3

((fib 3) + ((fib 1) + (fib 0))) + 3

((fib 3) + ((fib 1) + 1)) + 3

((fib 3) + (1 + 1)) + 3

((fib 3) + 2) + 3

(((fib 2) + (fib 1)) + 2) + 3

(((fib 2) + 1) + 2) + 3

(((fib 1) + (fib 0)) + 1) + 2) + 3

(((fib 1) + 1) + 1) + 2) + 3

(((1 + 1) + 1) + 2) + 3

((2 + 1) + 2) + 3

(3 + 2) + 3

5 + 3

8

引数5で22回の計算でした.

末尾再帰実行過程
fib 5

fib' 5 1 1

fib' (5 - 1) (1 + 1) 1

fib' (5 - 1) 2 1

fib' 4 2 1

fib' (4 - 1) (2 + 1) 2

fib' (4 - 1) 3 2

fib' 3 3 2

fib' (3 - 1) (3 + 2) 3

fib' (3 - 1) 5 3

fib' 2 5 3

fib' (2 - 1) (5 + 3) 3

fib' (2 - 1) 8 3

fib' 1 8 5

8

同じく引数5で14回の計算でした.

これは差が出て当然ですね......
申し訳ありませんでした.
また近いうちに, 計算量が同じ関数を用いて試したいと思います.
SaitoAtsushiさん, ありがとうございました.

8
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3