haskell の fib は遅くない
http://d.hatena.ne.jp/nishiohirokazu/20100622/1277208908
に驚愕するような話が書かれていて、要するに、
fib = 1:1:zipWith (+) fib (tail fib)
は遅いと。何に驚愕したかというと、3点しかとらずに、「O(N ** 2.6)である。」と結論づけていることだ。空間使用量が O(N**2) で増えていくコードなのだから、そんなの空間計算量が一時的に影響与えたと考えるのが自然だろうに、その可能性も潰さずに原因も考えずに結論を出している。そんな適当な実験をするんだ、ということだ。
ランダウの記号 O(f(n)) は n が無限大でどのような関数で押さえられるか、についての表現だ。なので実行時間を論じているときには、n が 10^6 みたいな小さい値の周辺でフィットする行為は実行時間のオーダーを推測する補助手段でしかない。
fib !! n は、理論的に実行時の動きを考えると O(n**2) になる。
遅延評価ではサンクをつくるがそれは定数倍しか時間に影響を与えないのは当然だと思う。
import System import List fib = 1:1:zipWith (+) fib (tail fib) main = do args <- getArgs print $ (0 *) $ (fib !!) $ read $ args !! 0
はじめにとったデータ。
10000 | 0.06 |
20000 | 0.16 |
30000 | 0.28 |
40000 | 0.44 |
50000 | 0.66 |
60000 | 0.9 |
70000 | 1.23 |
80000 | 1.62 |
90000 | 2.1 |
100000 | 2.56 |
110000 | 3.18 |
120000 | 3.92 |
130000 | 4.48 |
140000 | 5.31 |
150000 | 6.21 |
160000 | 7.24 |
170000 | 8.33 |
180000 | 9.27 |
190000 | 10.56 |
ここまでは、綺麗に O(n**2) にのっている。
perl -e '$i=10000;while($i<10000000){print "$i ".substr(`/usr/bin/time -o /dev/stdout -f "%e %U %S %t %K %p %F %R %c" ./a.out $i +RTS -K200M`,2);$i+=10000;}' | tee out.txt
(ただし、定数はこのままではない)
10000 | 0.03 | 0.01 | 0.00 | 0 | 0 | 0 | 0 | 797 | 11 |
20000 | 0.08 | 0.06 | 0.00 | 0 | 0 | 0 | 0 | 836 | 16 |
30000 | 0.19 | 0.16 | 0.00 | 0 | 0 | 0 | 0 | 937 | 37 |
40000 | 0.33 | 0.30 | 0.01 | 0 | 0 | 0 | 0 | 1277 | 50 |
50000 | 0.50 | 0.45 | 0.01 | 0 | 0 | 0 | 0 | 1297 | 83 |
60000 | 0.81 | 0.71 | 0.02 | 0 | 0 | 0 | 0 | 1572 | 137 |
70000 | 0.99 | 0.93 | 0.02 | 0 | 0 | 0 | 0 | 1978 | 75 |
80000 | 1.32 | 1.27 | 0.02 | 0 | 0 | 0 | 0 | 2270 | 53 |
90000 | 1.75 | 1.70 | 0.02 | 0 | 0 | 0 | 0 | 2271 | 46 |
100000 | 2.19 | 2.15 | 0.02 | 0 | 0 | 0 | 0 | 2564 | 25 |
200000 | 11.23 | 11.15 | 0.07 | 0 | 0 | 0 | 0 | 4398 | 32 |
300000 | 30.13 | 29.99 | 0.13 | 0 | 0 | 0 | 0 | 6782 | 68 |
400000 | 63.98 | 63.55 | 0.22 | 0 | 0 | 0 | 0 | 8004 | 739 |
500000 | 114.39 | 114.01 | 0.33 | 0 | 0 | 0 | 0 | 9736 | 706 |
600000 | 180.20 | 179.69 | 0.44 | 0 | 0 | 0 | 0 | 11987 | 892 |
700000 | 270.16 | 269.50 | 0.59 | 0 | 0 | 0 | 0 | 13974 | 1217 |
800000 | 409.71 | 408.42 | 0.74 | 0 | 0 | 0 | 0 | 15703 | 3437 |
900000 | 579.54 | 578.00 | 0.93 | 0 | 0 | 0 | 0 | 16923 | 4353 |
はじめは O(n**2) にのっているが、3*10**6 あたりからわずかにはずれる。ここをとりだして O(n**2) じゃないと騒いでたって事だね。
遅延評価だと(正格評価のつもりでいると予想しないところでメモリを食う、ので、メモリを食うとGCの影響やメモリ階層の影響がフクザツなので)実行時間が読みづらい、というのはあると思う。けど、実行される演算の回数は、必要な演算は結局最後には全部呼ばれるので、深く考えず普通に数えればok