Частота символов

Я пытаюсь найти частоту символов в файле, используя Haskell. Я хочу иметь возможность обрабатывать файлы размером ~ 500 МБ.

Что я пробовал до сих пор

  1. Он выполняет свою работу, но немного медленнее, поскольку анализирует файл 256 раз.

    calculateFrequency :: L.ByteString -> [(Word8, Int64)]
    calculateFrequency f = foldl (\acc x -> (x, L.count x f):acc) [] [255, 254.. 0]
    
  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
    

person Ravi Upadhyay    schedule 15.01.2014    source источник
comment
Что произойдет, если вы скомпилируете с ghc -O2? Оптимизация строгости, которая может избежать проблемы с памятью, может только вступить в силу.   -  person GS - Apologise to Monica    schedule 15.01.2014
comment
Все еще уходит из памяти.   -  person Ravi Upadhyay    schedule 15.01.2014
comment
Что если вы переключитесь на Data.Map.Strict: hackage.haskell.org/package/containers-0.5.0.0/docs/   -  person GS - Apologise to Monica    schedule 15.01.2014
comment
и помимо этого, возможно, используйте IntMap (вам нужно будет преобразовать Word8 в Int, но это должно быть нормально): hackage.haskell.org/package/containers-0.5.0.0/docs/   -  person GS - Apologise to Monica    schedule 15.01.2014
comment
@GaneshSittampalam у него не заканчивается память, но он все еще очень медленный.   -  person Ravi Upadhyay    schedule 15.01.2014
comment
Я предполагаю, что изменяемый массив из 256 элементов будет самой быстрой структурой данных.   -  person GS - Apologise to Monica    schedule 15.01.2014
comment
Вы хотите использовать строгий модуль Map в Data.Map.Strict.   -  person tibbe    schedule 15.01.2014


Ответы (4)


Ответ @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

Код @alex занимает (для моего примера файла) 24,81 сег, использование массива занимает 7,77 сег.

ОБНОВЛЕНО:

хотя решение 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

с ускорением ~50%.

ОБНОВЛЕНО:

Использование IOVector в качестве Snoyman соответствует версии 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]
person josejuan    schedule 15.01.2014

Вот реализация, использующая изменяемые неупакованные векторы вместо конструкций более высокого уровня. Он также использует conduit для чтения файла, чтобы избежать ленивого ввода-вывода.

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

Я запустил это на 500 МБ случайных данных и сравнил с ответом @josejuan на основе UArray:

  • основанные на каналах/изменяемые векторы: 1,006 с
  • UArray: 17,962 с

Я думаю, что должно быть возможно сохранить большую часть элегантности высокоуровневого подхода josejuan, но при этом сохранить скорость реализации изменяемого вектора, но у меня еще не было возможности попробовать реализовать что-то подобное. Также обратите внимание, что с некоторыми вспомогательными функциями общего назначения (такими как Data.ByteString.mapM или Data.Conduit.Binary.mapM) реализация может быть значительно проще без ущерба для производительности.

Вы также можете поэкспериментировать с этой реализацией в FP Haskell Center. .

EDIT: я добавил одну из этих отсутствующих функций в 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

Единственная разница в функциональности заключается в том, как печатается частота.

person Michael Snoyman    schedule 15.01.2014
comment
Есть ли причина использовать класс MonadIO вместо того, чтобы специализироваться на любом типе, ожидаемом runResourceT? Влияет ли это на производительность? - person Chris Taylor; 15.01.2014
comment
Очень красивое (впечатляющее) решение! - person josejuan; 15.01.2014
comment
@ChrisTaylor Нет, вы могли бы специализировать его на ResourceT IO. Или, если хотите, вы можете полностью избавиться от использования ResourceT, это просто немного удлинит код: gist .github.com/snoyberg/8436149 - person Michael Snoyman; 15.01.2014

Это работает для меня на моем компьютере:

module Main where
import qualified Data.HashMap.Strict as M
import qualified Data.ByteString.Lazy as L
import Data.Word
import Data.Int

calculateFrequency :: L.ByteString -> [(Word8, Int64)]
calculateFrequency xs = M.toList $ L.foldl' (\m word -> M.insertWith (+) word 1 m) M.empty xs

main = do
    bs <- L.readFile "E:\\Steam\\SteamApps\\common\\Sid Meier's Civilization V\\Assets\\DLC\\DLC_Deluxe\\Behind the Scenes\\Behind the Scenes.wmv"
    print (calculateFrequency bs)

Не заканчивается память и даже не загружается весь файл, но это занимает вечность (около минуты) для файлов размером более 600 МБ! Я скомпилировал это, используя ghc 7.6.3.

Я должен отметить, что код в основном идентичен, за исключением строгого HashMap вместо ленивого Map.

Обратите внимание, что insertWith в два раза быстрее с HashMap, чем Map в этом случае. На моей машине написанный код выполняется за 54 секунды, а версия с использованием Map занимает 107.

person Alex Reinking    schedule 15.01.2014
comment
Вы можете использовать Data.Map.Strict (единственное изменение, необходимое для исходного кода) - person josejuan; 15.01.2014
comment
^ Можно, но не следует. Я только что обновил свой ответ, указав некоторую информацию о времени выполнения. - person Alex Reinking; 15.01.2014
comment
Тогда вам не следует использовать HashMap, используйте STArray (MArray...) :) - person josejuan; 15.01.2014
comment
Да, наверное :) Если мы действительно хотим спуститься в кроличью нору, загляните на Data.HashTable.ST.Cuckoo - person Alex Reinking; 15.01.2014
comment
Итак, это самый быстрый Haskell? - person Ravi Upadhyay; 15.01.2014
comment
Я рискну сказать «нет». Но это примерно так же быстро, как и будет, будучи таким кратким. Вам придется использовать монаду IO или ST, чтобы получить O(1) операций поиска/вставки. - person Alex Reinking; 15.01.2014

Мои два цента (используя STUArray). Не могу сравнить это с другими решениями здесь. Может кто захочет попробовать...

module Main where

import Data.Array.ST (runSTUArray, newArray, readArray, writeArray)
import Data.Array.Unboxed (UArray)
import qualified Data.ByteString.Lazy as L (ByteString, unpack, getContents)
import Data.Word
import Data.Int
import Control.Monad (forM_)

calculateFrequency :: L.ByteString -> UArray Word8 Int64 
calculateFrequency bs = runSTUArray $ do
    a <- newArray (0, 255) 0
    forM_ (L.unpack bs) $ \i -> readArray a i >>= writeArray a i . succ
    return a

main = L.getContents >>= print . calculateFrequency
person Alfonso Villén    schedule 15.01.2014