Pergunta

Eu escrevi um analisador para um arquivo personalizado usando attoparsec.O relatório de criação de perfil indicou que cerca de 67% da alocação de memória é feita em uma função chamada tab, que também consome mais tempo.O tab a função é bem simples:

tab :: Parser Char
tab = char '\t'

Todo o relatório de perfil é o seguinte:

       ASnapshotParser +RTS -p -h -RTS

    total time  =       37.88 secs   (37882 ticks @ 1000 us, 1 processor)
    total alloc = 54,255,105,384 bytes  (excludes profiling overheads)

COST CENTRE    MODULE                %time %alloc

tab            Main                   83.1   67.7
main           Main                    6.4    4.2
readTextDevice Data.Text.IO.Internal   5.5   24.0
snapshotParser Main                    4.7    4.0


                                                             individual     inherited
COST CENTRE        MODULE                  no.     entries  %time %alloc   %time %alloc

MAIN               MAIN                     75           0    0.0    0.0   100.0  100.0
 CAF               Main                    149           0    0.0    0.0   100.0  100.0
  tab              Main                    156           1    0.0    0.0     0.0    0.0
  snapshotParser   Main                    153           1    0.0    0.0     0.0    0.0
  main             Main                    150           1    6.4    4.2   100.0  100.0
   doStuff         Main                    152     1000398    0.3    0.0    88.1   71.8
    snapshotParser Main                    154           0    4.7    4.0    87.7   71.7
     tab           Main                    157           0   83.1   67.7    83.1   67.7
   readTextDevice  Data.Text.IO.Internal   151       40145    5.5   24.0     5.5   24.0
 CAF               Data.Text.Array         142           0    0.0    0.0     0.0    0.0
 CAF               Data.Text.Internal      140           0    0.0    0.0     0.0    0.0
 CAF               GHC.IO.Handle.FD        122           0    0.0    0.0     0.0    0.0
 CAF               GHC.Conc.Signal         103           0    0.0    0.0     0.0    0.0
 CAF               GHC.IO.Encoding         101           0    0.0    0.0     0.0    0.0
 CAF               GHC.IO.FD               100           0    0.0    0.0     0.0    0.0
 CAF               GHC.IO.Encoding.Iconv    89           0    0.0    0.0     0.0    0.0
  main             Main                    155           0    0.0    0.0     0.0    0.0

Como faço para otimizar isso?

Todo o código pois o analisador está aqui. O arquivo que estou analisando tem cerca de 77 MB.

Foi útil?

Solução

tab é um bode expiatório.Se você definir boo :: Parser (); boo = return () e insira um boo antes de cada vínculo no snapshotParser definição, as alocações de custos se tornarão algo como:

 main             Main                    255           0   11.8   13.8   100.0  100.0
  doStuff         Main                    258     2097153    1.1    0.5    86.2   86.2
   snapshotParser Main                    260           0    0.4    0.1    85.1   85.7
    boo           Main                    262           0   71.0   73.2    84.8   85.5
     tab          Main                    265           0   13.8   12.3    13.8   12.3

Portanto, parece que o criador de perfil está transferindo a culpa pelas alocações dos resultados da análise, provavelmente devido à extensa inserção de attoparsec código, como John L sugeriu nos comentários.

Quanto aos problemas de desempenho, o ponto principal é que, ao analisar um arquivo de texto de 77 MB para construir uma lista com um milhão de elementos, você deseja que o processamento do arquivo seja lento e não rigoroso.Depois que isso for resolvido, desacoplar E/S e análise em doStuff e construir a lista de instantâneos sem acumulador também é útil.Aqui está uma versão modificada do seu programa levando isso em consideração.

{-# LANGUAGE BangPatterns #-}
module Main where

import Data.Maybe
import Data.Attoparsec.Text.Lazy
import Control.Applicative
import qualified Data.Text.Lazy.IO as TL
import Data.Text (Text)
import qualified Data.Text.Lazy as TL

buildStuff :: TL.Text -> [Snapshot]
buildStuff text = case maybeResult (parse endOfInput text) of
  Just _ -> []
  Nothing -> case parse snapshotParser text of
      Done !i !r -> r : buildStuff i
      Fail _ _ _ -> []

main :: IO ()
main = do
  text <- TL.readFile "./snap.dat"
  let ss = buildStuff text
  print $ listToMaybe ss
    >> Just (fromIntegral (length $ show ss) / fromIntegral (length ss))

newtype VehicleId = VehicleId Int deriving Show
newtype Time = Time Int deriving Show
newtype LinkID = LinkID Int deriving Show
newtype NodeID = NodeID Int deriving Show
newtype LaneID = LaneID Int deriving Show

tab :: Parser Char
tab = char '\t'

-- UNPACK pragmas. GHC 7.8 unboxes small strict fields automatically;
-- however, it seems we still need the pragmas while profiling. 
data Snapshot = Snapshot {
  vehicle :: {-# UNPACK #-} !VehicleId,
  time :: {-# UNPACK #-} !Time,
  link :: {-# UNPACK #-} !LinkID,
  node :: {-# UNPACK #-} !NodeID,
  lane :: {-# UNPACK #-} !LaneID,
  distance :: {-# UNPACK #-} !Double,
  velocity :: {-# UNPACK #-} !Double,
  vehtype :: {-# UNPACK #-} !Int,
  acceler :: {-# UNPACK #-} !Double,
  driver :: {-# UNPACK #-} !Int,
  passengers :: {-# UNPACK #-} !Int,
  easting :: {-# UNPACK #-} !Double,
  northing :: {-# UNPACK #-} !Double,
  elevation :: {-# UNPACK #-} !Double,
  azimuth :: {-# UNPACK #-} !Double,
  user :: {-# UNPACK #-} !Int
  } deriving (Show)

-- No need for bang patterns here.
snapshotParser :: Parser Snapshot
snapshotParser = do
  sveh <- decimal
  tab
  stime <- decimal
  tab
  slink <- decimal
  tab
  snode <- decimal
  tab
  slane <- decimal
  tab
  sdistance <- double
  tab
  svelocity <- double
  tab
  svehtype <- decimal
  tab
  sacceler <- double
  tab
  sdriver <- decimal
  tab
  spassengers <- decimal
  tab
  seasting <- double
  tab
  snorthing <- double
  tab
  selevation <- double
  tab
  sazimuth <- double
  tab
  suser <- decimal
  endOfLine <|> endOfInput
  return $ Snapshot
    (VehicleId sveh) (Time stime) (LinkID slink) (NodeID snode)
    (LaneID slane) sdistance svelocity svehtype sacceler sdriver
    spassengers seasting snorthing selevation sazimuth suser

Esta versão deve ter um desempenho aceitável mesmo se você forçar toda a lista de snapshots na memória, como fiz em main aqui.Para avaliar o que é "aceitável", tenha em mente que, dados os dezesseis campos (pequenos, sem caixa) em cada Snapshot mais o a sobrecarga do Snapshot e construtores de lista, estamos falando de 152 bytes por célula de lista, o que se resume a aproximadamente 152 MB para seus dados de teste.De qualquer forma, esta versão é o mais preguiçosa possível, como você pode ver removendo a divisão em main, ou substituindo-o por last ss.

Obs.:Meus testes foram feitos com attoparsec-0.12.

Outras dicas

Depois de atualizar o attoparsec para a versão mais recente (0.12.0.0), o tempo necessário para execução será reduzido de 38 segundos para 16 segundos.Isso é mais de 50% de aceleração.Além disso, a memória consumida por ele reduziu drasticamente.Como observou @JohnL, com o perfil habilitado, os resultados variam muito.Quando tentei criar um perfil com a versão mais recente da biblioteca attoparsec, demorou cerca de 64 segundos para executar todo o programa.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top