Плохой анализ производительности двоичного файла в haskell

У меня есть набор бинарных записей, упакованных в файл, и я читаю их с помощью Data.ByteString.Lazy и Data.Binary.Get. С моей текущей реализацией файл 8 Мб занимает 6 секунд для разбора.

import qualified Data.ByteString.Lazy as BL
import Data.Binary.Get

data Trade = Trade { timestamp :: Int, price :: Int ,  qty :: Int } deriving (Show)

getTrades = do
  empty <- isEmpty
  if empty
    then return []
    else do
      timestamp <- getWord32le          
      price <- getWord32le
      qty <- getWord16le          
      rest <- getTrades
      let trade = Trade (fromIntegral timestamp) (fromIntegral price) (fromIntegral qty)
      return (trade : rest)

main :: IO()
main = do
  input <- BL.readFile "trades.bin" 
  let trades = runGet getTrades input
  print $ length trades

Что я могу сделать, чтобы сделать это быстрее?

Ответы

Ответ 1

Ваш код декодирует 8 МБ файл менее чем за одну секунду (ghc-7.4.1) - конечно, я скомпилирован с -O2.

Однако для этого потребовалось чрезмерное количество пространства стека. Вы можете уменьшить

  • время
  • пространство стека
  • пустое пространство

требуемый путем добавления большей строгости в соответствующих местах и ​​использования аккумулятора для сбора разобранных сделок.

{-# LANGUAGE BangPatterns #-}
module Main (main) where

import qualified Data.ByteString.Lazy as BL
import Data.Binary.Get

data Trade = Trade { timestamp :: {-# UNPACK #-} !Int
                   , price :: {-# UNPACK #-} !Int 
                   , qty :: {-# UNPACK #-} !Int
                   } deriving (Show)

getTrades :: Get [Trade]
getTrades = go []
  where
    go !acc = do
      empty <- isEmpty
      if empty
        then return $! reverse acc
        else do
          !timestamp <- getWord32le
          !price <- getWord32le
          !qty <- getWord16le
          let !trade = Trade (fromIntegral timestamp) (fromIntegral price) (fromIntegral qty)
          go (trade : acc)

main :: IO()
main = do
  input <- BL.readFile "trades.bin"
  let trades = runGet getTrades input
  print $ length trades

Строгость и распаковка гарантируют, что никакая работа не будет отменена, чтобы вернуться, чтобы укусить вас позже, указав часть ByteString, которая уже должна была быть забыта.

Если вам нужно Trade иметь ленивые поля, вы все равно можете декодировать через тип со строгими полями и map преобразование по списку результатов, чтобы извлечь выгоду из более строгого декодирования.

Однако код все еще тратит много времени на сбор мусора, поэтому дальнейшие улучшения могут по-прежнему быть необходимы.

Ответ 2

Рефакторинг его немного (в основном с левым краем) дает намного лучшую производительность и снижает накладные расходы GC, немного разбирающий файл размером 8388600.

{-# LANGUAGE BangPatterns #-}
module Main (main) where

import qualified Data.ByteString.Lazy as BL
import Data.Binary.Get

data Trade = Trade
  { timestamp :: {-# UNPACK #-} !Int
  , price     :: {-# UNPACK #-} !Int 
  , qty       :: {-# UNPACK #-} !Int
  } deriving (Show)

getTrade :: Get Trade
getTrade = do
  timestamp <- getWord32le
  price     <- getWord32le
  qty       <- getWord16le
  return $! Trade (fromIntegral timestamp) (fromIntegral price) (fromIntegral qty)

countTrades :: BL.ByteString -> Int
countTrades input = stepper (0, input) where
  stepper (!count, !buffer)
    | BL.null buffer = count
    | otherwise      =
        let (trade, rest, _) = runGetState getTrade buffer 0
        in stepper (count+1, rest)

main :: IO()
main = do
  input <- BL.readFile "trades.bin"
  let trades = countTrades input
  print trades

И соответствующая статистика времени выполнения. Несмотря на то, что номера распределения близки, размер GC и максимальный размер кучи немного отличаются между версиями.

Все примеры здесь были построены с помощью GHC 7.4.1-O2.

Исходный источник, запускаемый с + RTS -K1G -RTS из-за чрезмерного использования пространства стека:

     426,003,680 bytes allocated in the heap
     443,141,672 bytes copied during GC
      99,305,920 bytes maximum residency (9 sample(s))
             203 MB total memory in use (0 MB lost due to fragmentation)

  Total   time    0.62s  (  0.81s elapsed)

  %GC     time      83.3%  (86.4% elapsed)

Редакция Daniel:

     357,851,536 bytes allocated in the heap
     220,009,088 bytes copied during GC
      40,846,168 bytes maximum residency (8 sample(s))
              85 MB total memory in use (0 MB lost due to fragmentation)

  Total   time    0.24s  (  0.28s elapsed)

  %GC     time      69.1%  (71.4% elapsed)

И этот пост:

     290,725,952 bytes allocated in the heap
         109,592 bytes copied during GC
          78,704 bytes maximum residency (10 sample(s))
               2 MB total memory in use (0 MB lost due to fragmentation)

  Total   time    0.06s  (  0.07s elapsed)

  %GC     time       5.0%  (6.0% elapsed)