字符频率

Rav*_*yay 5 haskell bytestring

我试图使用Haskell找到文件中的字符频率.我希望能够处理大小约500MB的文件.

我到现在为止做了什么

  1. 它完成了这项工作但是有点慢,因为它解析了256次文件

    calculateFrequency :: L.ByteString -> [(Word8, Int64)]
    calculateFrequency f = foldl (\acc x -> (x, L.count x f):acc) [] [255, 254.. 0]
    
    Run Code Online (Sandbox Code Playgroud)
  2. 我也尝试过使用Data.Map,但程序内存耗尽(在ghc解释器中).

    import qualified Data.ByteString.Lazy as L
    import qualified Data.Map as M
    
    calculateFrequency' :: L.ByteString -> [(Word8, Int64)]
    calculateFrequency' xs = M.toList $ L.foldl' (\m word -> M.insertWith (+) word 1 m) (M.empty) xs
    
    Run Code Online (Sandbox Code Playgroud)

Mic*_*man 14

这是一个使用可变的,未装箱的向量而不是更高级别的构造的实现.它还conduit用于读取文件以避免惰性I/O.

import           Control.Monad.IO.Class
import qualified Data.ByteString             as S
import           Data.Conduit
import           Data.Conduit.Binary         as CB
import qualified Data.Conduit.List           as CL
import qualified Data.Vector.Unboxed.Mutable as VM
import           Data.Word                   (Word8)

type Freq = VM.IOVector Int

newFreq :: MonadIO m => m Freq
newFreq = liftIO $ VM.replicate 256 0

printFreq :: MonadIO m => Freq -> m ()
printFreq freq =
    liftIO $ mapM_ go [0..255]
  where
    go i = do
        x <- VM.read freq i
        putStrLn $ show i ++ ": " ++ show x

addFreqWord8 :: MonadIO m => Freq -> Word8 -> m ()
addFreqWord8 f w = liftIO $ do
    let index = fromIntegral w
    oldCount <- VM.read f index
    VM.write f index (oldCount + 1)

addFreqBS :: MonadIO m => Freq -> S.ByteString -> m ()
addFreqBS f bs =
    loop (S.length bs - 1)
  where
    loop (-1) = return ()
    loop i = do
        addFreqWord8 f (S.index bs i)
        loop (i - 1)

-- | The main entry point.
main :: IO ()
main = do
    freq <- newFreq
    runResourceT
        $  sourceFile "random"
        $$ CL.mapM_ (addFreqBS freq)
    printFreq freq
Run Code Online (Sandbox Code Playgroud)

我用500MB的随机数据运行这个,并与@ josejuan基于UArray的答案进行比较:

  • 基于导管/可变载体:1.006s
  • UArray:17.962s

我认为应该可以保持josejuan的高级方法的优雅,同时保持可变矢量实现的速度,但我还没有机会尝试实现类似的东西.此外,请注意,对于一些通用帮助函数(如Data.ByteString.mapM或Data.Conduit.Binary.mapM),实现可能会非常简单,而不会影响性能.

您也可以在FP Haskell Center上使用此实现.

编辑:我添加了一个缺少的函数,conduit并清理了一些代码; 它现在看起来如下:

import           Control.Monad.Trans.Class   (lift)
import           Data.ByteString             (ByteString)
import           Data.Conduit                (Consumer, ($$))
import qualified Data.Conduit.Binary         as CB
import qualified Data.Vector.Unboxed         as V
import qualified Data.Vector.Unboxed.Mutable as VM
import           System.IO                   (stdin)

freqSink :: Consumer ByteString IO (V.Vector Int)
freqSink = do
    freq <- lift $ VM.replicate 256 0
    CB.mapM_ $ \w -> do
        let index = fromIntegral w
        oldCount <- VM.read freq index
        VM.write freq index (oldCount + 1)
    lift $ V.freeze freq

main :: IO ()
main = (CB.sourceHandle stdin $$ freqSink) >>= print
Run Code Online (Sandbox Code Playgroud)

功能的唯一区别在于如何打印频率.


jos*_*uan 6

@Alex答案很好但是,只有256个值(索引),数组应该更好

import qualified Data.ByteString.Lazy as L
import qualified Data.Array.Unboxed as A
import qualified Data.ByteString as B
import Data.Int
import Data.Word

fq :: L.ByteString -> A.UArray Word8 Int64
fq = A.accumArray (+) 0 (0, 255) . map (\c -> (c, 1)) . concat . map B.unpack . L.toChunks

main = L.getContents >>= print . fq
Run Code Online (Sandbox Code Playgroud)

@alex代码取(对于我的示例文件)24.81 segs,使用数组取7.77 segs.

更新:

虽然Snoyman解决方案更好,但unpack可能会有所改善

fq :: L.ByteString -> A.UArray Word8 Int64
fq = A.accumArray (+) 0 (0, 255) . toCounterC . L.toChunks
     where toCounterC [] = []
           toCounterC (x:xs) = toCounter x (B.length x) xs
           toCounter  _ 0 xs = toCounterC xs
           toCounter  x i xs = (B.index x i', 1): toCounter x i' xs
                               where i' = i - 1
Run Code Online (Sandbox Code Playgroud)

加速度提高约50%.

更新:

使用IOVectorSnoyman就像Conduit版本一样(实际上要快一点,但这是一个原始代码,更好用Conduit)

import           Data.Int
import           Data.Word
import           Control.Monad.IO.Class
import qualified Data.ByteString.Lazy          as L
import qualified Data.Array.Unboxed            as A
import qualified Data.ByteString               as B
import qualified Data.Vector.Unboxed.Mutable   as V

fq :: L.ByteString -> IO (V.IOVector Int64)
fq xs =
     do
       v <- V.replicate 256 0 :: IO (V.IOVector Int64)
       g v $ L.toChunks xs
       return v
     where g v = toCounterC
                 where toCounterC [] = return ()
                       toCounterC (x:xs) = toCounter x (B.length x) xs
                       toCounter  _ 0 xs = toCounterC xs
                       toCounter  x i xs = do
                                             let i' = i - 1
                                                 w  = fromIntegral $ B.index x i'
                                             c <- V.read v w
                                             V.write v w (c + 1)
                                             toCounter x i' xs

main = do
          v <- L.getContents >>= fq
          mapM_ (\i -> V.read v i >>= liftIO . putStr . (++", ") . show) [0..255]
Run Code Online (Sandbox Code Playgroud)