Haskellでtailコマンド - その2
前回作ったtailがあまりにも遅過ぎて酷かったので、全面的に書き直しました。
mmapは使ってません。conduitは使ってます。
{- - 方針 - Sourceでは、seekして後ろから4096バイトずつ(又はファイルサイズの1/10バイトずつ)取得する。 - Conduitで、後ろからn個目の改行がある所までで入力を切る。 - Sinkは後ろから順に積んでいく。 -} import Control.Applicative ((<$>)) import Control.Monad (when) import Control.Monad.IO.Class (liftIO) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BC import Data.Conduit ( ($$), ($=) ) import qualified Data.Conduit as C import System.Environment (getArgs) import qualified System.IO as SI type FileSize = Integer readLen :: FileSize -> Integer readLen s | s < std = std | otherwise = if x > limit then limit else x where x = s `div` 10 std = 4096 limit = 10 * 1024 * 1024 reverseFile :: C.ResourceIO m => FilePath -> C.Source m BS.ByteString reverseFile fp = C.sourceIO initialize close pull where initialize = do h <- SI.openBinaryFile fp SI.ReadMode s <- readLen <$> SI.hFileSize h b <- SI.hIsSeekable h when b $ SI.hSeek h SI.SeekFromEnd (-1) return $ if b then Just (h,s) else Nothing close st | Nothing <- st = return () | Just (h,_) <- st = SI.hClose h pull st | Nothing <- st = return C.Closed | Just (h,l) <- st = do pos <- liftIO $ SI.hTell h if pos == 0 then return C.Closed else do let len = if pos < l then pos else l liftIO $ SI.hSeek h SI.RelativeSeek (-len) x <- liftIO $ BS.hGetSome h $ fromIntegral len liftIO $ SI.hSeek h SI.RelativeSeek (-len) return $ C.Open x stackSink :: C.Resource m => C.Sink BS.ByteString m BS.ByteString stackSink = C.sinkState BS.empty push close where close = return push st i = return (i `BS.append` st, C.Processing) limitLinesReverse :: C.Resource m => Int -> C.Conduit BS.ByteString m BS.ByteString limitLinesReverse count = C.conduitState count push close where close _ = return [] push n i | n == 0 = return (n, C.Finished (Just i) []) | otherwise = do let (n',bs) = loop n i if n' == 0 then return (n', C.Finished Nothing [bs]) else return (n', C.Producing [bs]) loop n x | c <= n = (n - c, x) | c > n = (0, BC.drop (pos - 1) x) where c = BC.count '\n' x pos = BC.elemIndices '\n' x !! (c - n) main :: IO () main = do [fp, n] <- getArgs x <- C.runResourceT $ reverseFile fp $= limitLinesReverse (read n) $$ stackSink BC.putStrLn $ BC.drop 1 x
これをghc -O2 -o mytail でコンパイルしてOpenSuseのtailと比較してみました。
結果:
対象データ 18MiB / 381091行 のテキストファイル(.emacsをずらずら繋げて作成) 500行読み込み 自作tail : ./mytail ~/test.txt 500 0.00s user 0.00s system 60% cpu 0.012 total Suseのtail : tail -n 500 ~/test.txt 0.00s user 0.00s system 15% cpu 0.007 total 1000行読み込み 自作tail : ./mytail ~/test.txt 1000 0.00s user 0.01s system 41% cpu 0.029 total Suseのtail : tail -n 1000 ~/test.txt 0.00s user 0.00s system 19% cpu 0.016 total 5000行読み込み 自作tail : ./mytail ~/test.txt 5000 0.00s user 0.01s system 18% cpu 0.082 total Suseのtail : tail -n 5000 ~/test.txt 0.00s user 0.01s system 8% cpu 0.068 total 100000行読み込み 自作tail : ./mytail ~/test.txt 100000 0.01s user 0.21s system 14% cpu 1.438 total Suseのtail : tail -n 100000 ~/test.txt 0.01s user 0.18s system 12% cpu 1.433 total 381091行(全部)読み込み 自作tail : ./mytail ~/test.txt 381091 0.04s user 0.69s system 13% cpu 5.546 total Suseのtail : tail -n 381091 ~/test.txt 0.01s user 0.82s system 15% cpu 5.446 total
今日は気持ちよく寝れそうです。