遅延評価の落とし穴
http://www.bagley.org/~doug/shootout/
このページ、言語の比較ページなのであるが、
Haskellのスコアが不当に低いように感じた。
全体のスコアは未実装のテストが有ると大幅に低下してしまうようなのだが、
LOC(コードの長さ)でもあまり芳しくないのはどうしたものかと思った。
いや、全体的には順位が高いのだが、
一部のテストが長くなりすぎている。
それで、適当に長くなっているのを見てみると、
http://www.bagley.org/~doug/shootout/bench/wc/
この「Count Lines/Words/Chars」のコードを見て唖然とさせられた。
勝手に貼っていいのかわからないが…
-- $Id: wc.ghc,v 1.2 2001/05/24 14:05:53 doug Exp $ -- http://www.bagley.org/~doug/shootout/ -- from Brian Gregor module Main where -- compile with: ghc -O -o wc -package lang wc.hs import IO import IOExts import PackedString main = do -- set buffer to 4k hSetBuffering stdin (BlockBuffering (Just 4096)) -- these are mutable variables nL <- newIORef 0 nW <- newIORef 0 nC <- newIORef 0 (nl,nw,nc) <- countAll nL nW nC putStrLn ( (show nl)++" "++(show nw)++" "++(show nc) ) countAll :: IORef Int -> IORef Int -> IORef Int -> IO (Int,Int,Int) countAll nL nW nC = do end <- hIsEOF stdin nl <- readIORef nL nw <- readIORef nW nc <- readIORef nC if (not end) then (do inStr <- hGetLine stdin -- using a packed string is a small speed win let str = packString inStr -- using IORefs makes it easy to force strict -- evaluation - how to easily do this without -- IORefs? writeIORef nL $! (nl + 1) writeIORef nW $! (nw + (length (wordsPS str))) writeIORef nC $! (nc + 1 + (lengthPS str)) countAll nL nW nC) else return (nl,nw,nc)
これはひどい。
何でこんなにImperativeに書かれているのだろうか。
このテスト、"same thing"タイプのテストになっていて、
同じことが出来るソースならどんな実装でも良いはずなので、
適当に簡潔に書き直してみた。
main = interact $ \d -> tail $ concat $ map ( (" "++).show.($d) ) [length.lines,length.words,length]
二行である。
下のコードで問題になりそうなのは実行速度とメモリ消費量である。
入力は遅延するとはいえ、最終的に全域を3回なめるので
結局完全なリストが保持されてしまう。
入力ファイルは2MB以上あるので、(おとといぐらいの考察より
id:tanakh:20040725)おそらく50MB以上のメモリを消費する。
そこで、実際に実行してみた。
$ ghc -O2 count.hs $ time ./a.out < Input500 12500 68500 3048000 real 0m5.068s user 0m0.010s sys 0m0.020s
これだけでは如何ともなので、
最初のソースでも計測してみる。
$ ghc -O2 count.hs $ time ./a.out < Input500 12500 68500 3048000 real 0m9.187s user 0m0.010s sys 0m0.020s
なんと、2行のソースのほうが実行時間が短い。
じゃあ、やっぱり最初のソースは駄目駄目かというと
メモリを5MBほどしか使わない。
2行のやつは90MB近く消費するのである。
しかし、いくらなんでもあれではなぁ、
ということで、高速で簡潔なコードを模索することにした。
import System.IO main = do (l,w,c) <- count (0,0,0) putStrLn $ show l ++ " " ++ show w ++ " " ++ show c count cs@(l,w,c) = do eof <- hIsEOF stdin if eof then return cs else do d <- getLine let nw = length $ words d nc = length d seq nw $ seq nc $ count $! (l+1,w+nw,c+nc)
その結果得られたのがこのコードである(簡潔じゃないけど)。
(このコード、出来上がるまで5時間ぐらいかかったんだけど…)
要するに2行のやつを行ごとの処理に分けただけなのであるが、
これ、
count cs@(l,w,c) = do eof <- hIsEOF stdin if eof then return cs else do d <- getLine count (l+1,w+(length $ words d),c+(length d))
のような直接的なコードだと駄目である。
遅延してしまうので結局同じぐらいのメモリを食ってしまう。
なので、ところどころに正格性評価を行うための処理を織り込んでいる。
気になる速度とメモリ消費量であるが、
$ ghc -O2 count.hs $ time ./a.out < Input500 12500 68500 3035500 real 0m2.257s user 0m0.010s sys 0m0.020s
こんな感じ。
メモリ消費量は4MBほどである。
これだとコードの長さも12行だし、
うちのマシンの性能とから考えるとスピードでもpython〜bigloo〜se
あたりには食い込めそうである。
pythonレベルといっても、主に入出力+文字列処理なので、
g++の3分の一ほどの速度である。
(gcc/ocamlは相変わらず信じられないほど速いけど)
一つ大きな問題は、
こんな直感的ではない処理をはさまなければならないということか。
Haskellでは大きなデータを扱うのは鬼門のようである…