Понимание использования памяти этой программой Haskell

Я должен предварить это, сказав, что я очень новичок в Haskell и библиотеке каналов, и я хотел бы понять, что вызывает высокое использование памяти этой программой в функции test.

В частности, в сгибе, который создает значение r1 в test, я вижу накопление значений MyRecord до тех пор, пока не будет получен окончательный результат, если только не используется deepseq. В моем тестовом наборе данных ~ 500 000 строк / ~ 230 МБ использование памяти превышает 1,5 ГБ.

Свертка, производящая значение r2, выполняется в постоянной памяти.

Что я хотел бы понять:

1) Что может быть причиной построения значений MyMemory в первом фолде и почему использование deepseq исправит это? Я очень сильно бросал вещи наугад, пока не пришел к использованию deepseq для достижения постоянного использования памяти, но хотел бы понять, почему это работает. Можно ли добиться постоянного использования памяти без использования deepseq, при этом производя результат того же типа, что и Maybe Int?

2). Чем отличается вторая складка, из-за которой не возникает та же проблема?

Я знаю, что если бы мне пришлось работать только с целыми числами вместо кортежей, я мог бы использовать встроенную функцию sum из Pipes.Prelude, но в конечном итоге я захочу обработать второй элемент, который содержит какие-либо ошибки синтаксического анализа.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Test where

import           Control.Arrow
import           Control.DeepSeq
import           Control.Monad
import           Data.Aeson
import           Data.Function
import           Data.Maybe
import           Data.Monoid
import           Data.Text (Text)

import           Pipes
import qualified Pipes.Aeson as PA (DecodingError(..))
import qualified Pipes.Aeson.Unchecked as PA
import qualified Pipes.ByteString as PB
import qualified Pipes.Group as PG
import qualified Pipes.Parse as PP
import qualified Pipes.Prelude as P

import           System.IO
import           Control.Lens
import qualified Control.Foldl as Fold

data MyRecord = MyRecord
  { myRecordField1 :: !Text
  , myRecordField2 :: !Int
  , myRecordField3 :: !Text
  , myRecordField4 :: !Text
  , myRecordField5 :: !Text
  , myRecordField6 :: !Text
  , myRecordField7 :: !Text
  , myRecordField8 :: !Text
  , myRecordField9 :: !Text
  , myRecordField10 :: !Int
  , myRecordField11 :: !Text
  , myRecordField12 :: !Text
  , myRecordField13 :: !Text
  } deriving (Eq, Show)

instance FromJSON MyRecord where
  parseJSON (Object o) =
    MyRecord <$> o .: "field1" <*> o .: "field2" <*> o .: "field3" <*>
    o .: "field4" <*>
    o .: "field5" <*>
    o .: "filed6" <*>
    o .: "field7" <*>
    o .: "field8" <*>
    o .: "field9" <*>
    (read <$> o .: "field10") <*>
    o .: "field11" <*>
    o .: "field12" <*>
    o .: "field13"
  parseJSON x = fail $ "MyRecord: expected Object, got: " <> show x

instance ToJSON MyRecord where
    toJSON _ = undefined

test :: IO ()
test = do
  withFile "some-file" ReadMode $ \hIn
  {-

      the pipeline is composed as follows:

      1 a producer reading a file with Pipes.ByteString, splitting chunks into lines,
        and parsing the lines as JSON to produce tuples of (Maybe MyRecord, Maybe
        ByteString), the second element being an error if parsing failed

      2 a pipe filtering that tuple on a field of Maybe MyRecord, passing matching
        (Maybe MyRecord, Maybe ByteString) downstream

      3 and a pipe that picks an Int field out of Maybe MyRecord, passing (Maybe Int,
        Maybe ByteString downstream)

      pipeline == 1 >-> 2 >-> 3

      memory profiling indicates the memory build up is due to accumulation of
      MyRecord "objects", and data types comprising their fields (mainly
      Text/ARR_WORDS)

  -}
   -> do
    let pipeline = f1 hIn >-> f2 >-> f3
    -- need to use deepseq to avoid leaking memory
    r1 <-
      P.fold
        (\acc (v, _) -> (+) <$> acc `deepseq` acc <*> pure (fromMaybe 0 v))
        (Just 0)
        id
        (pipeline :: Producer (Maybe Int, Maybe PB.ByteString) IO ())
    print r1
    hSeek hIn AbsoluteSeek 0
    -- this works just fine as is and streams in constant memory
    r2 <-
      P.fold
        (\acc v ->
           case fst v of
             Just x -> acc + x
             Nothing -> acc)
        0
        id
        (pipeline :: Producer (Maybe Int, Maybe PB.ByteString) IO ())
    print r2
    return ()
  return ()

f1
  :: (FromJSON a, MonadIO m)
  => Handle -> Producer (Maybe a, Maybe PB.ByteString) m ()
f1 hIn = PB.fromHandle hIn & asLines & resumingParser PA.decode

f2
  :: Pipe (Maybe MyRecord, Maybe PB.ByteString) (Maybe MyRecord, Maybe PB.ByteString) IO r
f2 = filterRecords (("some value" ==) . myRecordField5)

f3 :: Pipe (Maybe MyRecord, d) (Maybe Int, d) IO r
f3 = P.map (first (fmap myRecordField10))

filterRecords
  :: Monad m
  => (MyRecord -> Bool)
  -> Pipe (Maybe MyRecord, Maybe PB.ByteString) (Maybe MyRecord, Maybe PB.ByteString) m r
filterRecords predicate =
  for cat $ \(l, e) ->
    when (isNothing l || (predicate <$> l) == Just True) $ yield (l, e)

asLines
  :: Monad m
  => Producer PB.ByteString m x -> Producer PB.ByteString m x
asLines p = Fold.purely PG.folds Fold.mconcat (view PB.lines p)

parseRecords
  :: (Monad m, FromJSON a, ToJSON a)
  => Producer PB.ByteString m r
  -> Producer a m (Either (PA.DecodingError, Producer PB.ByteString m r) r)
parseRecords = view PA.decoded

resumingParser
  :: Monad m
  => PP.StateT (Producer a m r) m (Maybe (Either e b))
  -> Producer a m r
  -> Producer (Maybe b, Maybe a) m ()
resumingParser parser p = do
  (x, p') <- lift $ PP.runStateT parser p
  case x of
    Nothing -> return ()
    Just (Left _) -> do
      (x', p'') <- lift $ PP.runStateT PP.draw p'
      yield (Nothing, x')
      resumingParser parser p''
    Just (Right b) -> do
      yield (Just b, Nothing)
      resumingParser parser p'

person ppb    schedule 06.09.2016    source источник
comment
Ознакомьтесь с разделом информации о теге haskell и, пожалуйста, напишите, как вы компилируете и запускаете свой двоичный файл.   -  person jberryman    schedule 06.09.2016
comment
Потому что seq (Just undefined) = () но seq (undefined :: Int) () = undefined   -  person user2407038    schedule 06.09.2016
comment
Рассмотрим forceMaybe Nothing = Nothing; forceMaybe x@(Just !_) = x.   -  person dfeuer    schedule 06.09.2016
comment
Спасибо за комментарии всем! Если я понимаю, о чем идет речь, проблема связана с накоплением результата в Maybe и функцией свертывания, создающей зависимость или ссылку на значения MyRecord. До тех пор, пока не будет запрошен результат всей складки, промежуточные значения не будут оцениваться за пределами конструктора Maybe, что приведет к накоплению MyRecord. Deepseq заставляет все, что прячется в Maybe, и позволяет MyRecord собирать мусор. Будет ли это справедливой оценкой происходящего?   -  person ppb    schedule 06.09.2016
comment
Кстати, вы можете использовать P.sum (pipeline >-> P.map (fromMaybe 0 . fst)).   -  person Gurkenglas    schedule 07.09.2016


Ответы (1)


Как указано в документах для Pipes.foldl, складка строгая. Однако строгость реализована с помощью $!< /a>, что приводит к оценке только WHNF - слабая нормальная форма головы. WHNF достаточно, чтобы полностью оценить простой тип, такой как Int, но недостаточно сильный, чтобы полностью оценить более сложный тип, такой как Maybe Int.

Несколько примеров:

main1 = do
  let a = 3 + undefined
      b = seq a 10
  print b                -- error: Exception: Prelude.undefined

main2 = do
  let a = Just (3 + undefined)
      b = seq a 10
  print b                -- no exception

В первом случае переменная acc представляет собой Just большого блока - сумму всех элементов. На каждой итерации переменная acc изменяется от Just a к Just (a+b), к Just (a+b+c) и т. д. Сложение не выполняется во время свертывания — оно выполняется только в самом конце. Большое использование памяти происходит из-за хранения этой растущей суммы в памяти.

Во втором случае сумма уменьшается на каждой итерации на $! до простого Int.

Помимо использования deepseq вы также можете использовать force:

force x = x `deepseq` x

и как упоминается в документах deepseq в сочетании с ViewPatterns вы можете создать шаблон, который будет полностью оценивать аргумент функции:

{-# LANGUAGE ViewPatterns #-}

...
P.fold
  (\(force -> !acc) (v,_) -> (+) <$> acc <*> pure (fromMaybe 0 v))
  (Just 0)
  ...
person ErikR    schedule 06.09.2016
comment
Спасибо @ErikR Правильно ли я думаю, что накопление этих преобразователь в накопителе сгиба также будет заставить не выпускать MyRecord? - person ppb; 06.09.2016
comment
Да, потому что суммирование содержит ссылки на ваши значения MyRecord через вызовы myRecordField10. - person ErikR; 07.09.2016