Pregunta

Escribí un analizador para un archivo personalizado usando attoparsec.El informe de creación de perfiles indicó que alrededor del 67% de la asignación de memoria se realiza en una función denominada tab, que también consume la mayor cantidad de tiempo.El tab La función es bastante simple:

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

El informe completo del perfil es el siguiente:

       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

¿Cómo optimizo esto?

El código completo porque el analizador está aquí. El archivo que estoy analizando tiene alrededor de 77 MB.

¿Fue útil?

Solución

tab es un chivo expiatorio.si defines boo :: Parser (); boo = return () e inserte un boo antes de cada enlace en el snapshotParser Por definición, las asignaciones de costos serán 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

Entonces parece que el generador de perfiles está echando la culpa por las asignaciones de los resultados del análisis, probablemente debido a la extensa inclusión de attoparsec código, como sugirió John L en los comentarios.

En cuanto a los problemas de rendimiento, el punto clave es que, mientras analiza un archivo de texto de 77 MB para crear una lista con un millón de elementos, desea que el procesamiento del archivo sea lento y no estricto.Una vez que esto esté resuelto, desacoplar E/S y analizar doStuff y crear la lista de instantáneas sin un acumulador también son útiles.Aquí hay una versión modificada de su programa teniendo eso en cuenta.

{-# 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 versión debería tener un rendimiento aceptable incluso si fuerza la lista completa de instantáneas en la memoria, como hice en main aquí.Para evaluar qué es "aceptable", tenga en cuenta que, dados los dieciséis campos (pequeños, sin recuadros) en cada Snapshot más el gastos generales del Snapshot y constructores de listas, estamos hablando de 152 bytes por celda de lista, lo que se reduce a ~152 MB para los datos de prueba.En cualquier caso, esta versión es lo más vaga posible, como se puede ver al eliminar la división en main, o reemplazándolo por last ss.

NÓTESE BIEN.:Mis pruebas se realizaron con attoparsec-0.12.

Otros consejos

Después de actualizar attoparsec a la última versión (0.12.0.0), el tiempo necesario para ejecutarse se reduce de 38 segundos a 16 segundos.Eso es más del 50% de aceleración.Además, la memoria consumida por él se redujo drásticamente.Como señaló @JohnL, con la creación de perfiles habilitada, los resultados varían enormemente.Cuando intenté crear un perfil con la última versión de la biblioteca attoparsec, me llevó alrededor de 64 segundos ejecutar todo el programa.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top