У меня есть конвейер pipeline, обрабатывающий длинный файл. Я хочу печатать отчет о проделанной работе для пользователя каждые 1000 записей, поэтому написал следующее:
-- | Every n records, perform the IO action.
-- Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = skipN n 1
where
skipN c t = do
mv <- await
case mv of
Nothing -> return ()
Just v ->
if c <= 1
then do
liftIO $ act t v
yield v
skipN n (succ t)
else do
yield v
skipN (pred c) (succ t)
Независимо от того, какое действие я вызываю, это приводит к утечке памяти, даже если я просто приказываю ему напечатать точку.
Насколько я могу судить, функция является хвостовой рекурсивной, и оба счетчика регулярно форсируются (я пытался ввести «seq c» и «seq t», но безрезультатно). Есть подсказка?
Если я введу "awaitForever", который печатает отчет для каждой записи, тогда он будет работать нормально.
Обновление 1: это происходит только при компиляции с -O2. Профилирование указывает, что текущая память выделяется в рекурсивной функции «skipN» и сохраняется «СИСТЕМОЙ» (что бы это ни значило).
Обновление 2: мне удалось вылечить его, по крайней мере, в контексте моей текущей программы. Я заменил указанную выше функцию на это. Обратите внимание, что «proc» имеет тип «Int -> Int -> Maybe i -> m ()»: чтобы использовать его, вы вызываете «await» и передаете ему результат. По какой-то причине замена await и yield решила проблему. Итак, теперь он ожидает следующего ввода, прежде чем выдаст предыдущий результат.
-- | Every n records, perform the monadic action.
-- Used for progress reports to the user.
progress :: (MonadIO m) => Int -> (Int -> i -> IO ()) -> Conduit i m i
progress n act = await >>= proc 1 n
where
proc c t = seq c $ seq t $ maybe (return ()) $ \v ->
if c <= 1
then {-# SCC "progress.then" #-} do
liftIO $ act t v
v1 <- await
yield v
proc n (succ t) v1
else {-# SCC "progress.else" #-} do
v1 <- await
yield v
proc (pred c) (succ t) v1
Поэтому, если у вас есть утечка памяти в Conduit, попробуйте поменять местами yield и await actions.
skipN
, а к(>>) (yield v) (skipN x y)
. Это распространенная ошибка при написании рекурсивных подпрограмм с использованием монад. Я не уверен, сможет ли GHC оптимизировать это правильно, не глядя на дамп ядра, но я предполагаю, что вы на самом деле не используете хвостовую рекурсивную функцию. - person bheklilr   schedule 16.07.2014m>>n
может не вызыватьn
в хвостовой позиции? - person dfeuer   schedule 16.07.2014sum (x:xs) = x + sum xs
не является хвостовой рекурсией, последняя вызываемая функция неsum
, а(+)
, поскольку она эквивалентнаsum (x:xs) = (+) x xs
. Вот почему мы часто пишем рекурсивные функции, используя вспомогательную функцию с аргументом аккумулятора, или просто используемfold
s, если ситуация достаточно проста, например,sum = go 0 where { go a [] = a; go a (x:xs) a = go (x + a) xs }
илиsum = foldl' (+) 0
. Поскольку в нотации do используются десахары>>
и>>=
, это означает, что последний вызов в стеке относится к одному из них, а не к его второму аргументу. - person bheklilr   schedule 16.07.2014(+)
необходимо проверить свой второй аргумент, прежде чем он сможет избавиться от своего первого. Я ничего не знаю о каналах, но вIO
,m >>= f
переводится в код, который выглядит как 1. Выполнитьm
(без хвоста). Результат выложите вr
. 2. Выполнитеf r
(хвост). К моменту проверкиf
отm
не остается ничего, кроме результата, который он произвел, и (что более важно)>>=
не должен ничего делать сf r
; его работа уже сделана. Очевидно, что это не так для всех монад; может что-то про проводники его ломает? - person dfeuer   schedule 16.07.2014pipes
, ниconduit
не должны быть хвостовой рекурсией для работы в постоянном пространстве. Хвостовая рекурсивная дискуссия - просто отвлекающий маневр. - person Gabriel Gonzalez   schedule 16.07.2014IO
могут быть сделаны особые оптимизации по сравнению с пользовательскими монадами, посколькуIO
- очень особенная монада низкого уровня, но для пользовательских монад, я полагаю, может быть важно рассмотреть такие вещи, как это. - person bheklilr   schedule 16.07.2014progress
, который демонстрирует утечку памяти. - person Tom Ellis   schedule 17.07.2014t
никогда не будет принудительным, еслиact
это не заставит? Печать точки этого не сделает ... - person Tom Ellis   schedule 17.07.2014skipN c t
наskipN !c !t
и посмотрите, поможет ли это. - person ozataman   schedule 17.07.2014Int
арифметических операций совпадает, а начальныйt
аргумент дляskipN
локально известен как1
, поэтому компилятор может безопасно сделать его строгим вt
. Я не уверен, так это или нет, и для этого, безусловно, потребуется оптимизация. - person dfeuer   schedule 17.07.2014