Почему это вызывает утечку памяти в библиотеке Haskell Conduit?
У меня есть conduit конвейер, обрабатывающий длинный файл. Я хочу распечатать отчет о проделанной работе для каждого 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" и сохраняется "SYSTEM" (что бы это ни значило).
Обновление 2: мне удалось вылечить его, по крайней мере, в контексте моей текущей программы. Я заменил эту функцию выше. Обратите внимание, что "proc" имеет тип "Int → Int → Maybe я → m()": для его использования вы вызываете "ожидание" и передаете ему результат. По какой-то причине переключение на "ожидание" и "выход" решило проблему. Итак, теперь он ждет следующего ввода, прежде чем уступить предыдущему результату.
-- | 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
Итак, если у вас есть утечка памяти в кабелепроводе, попробуйте заменить выход и ждать действий.
Ответы
Ответ 1
Это не ответ, но это полный код, который я взломал для тестирования. Я вообще не знаю канал, так что, возможно, это не лучший код канала. Я вынудил все, что кажется, должно быть принуждено, но это все еще протекает.
{-# LANGUAGE BangPatterns #-}
import Data.Conduit
import Data.Conduit.List
import Control.Monad.IO.Class
-- | 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 :: Int) <= 1
then do
liftIO $ act t v
yield v
skipN n (succ t)
else do
yield v
skipN (pred c) (succ t)
main :: IO ()
main = unfold (\b -> b 'seq' Just (b, b+1)) 1
$= progress 100000 (\_ b -> print b)
$$ fold (\_ _ -> ()) ()
С другой стороны,
main = unfold (\b -> b 'seq' Just (b, b+1)) 1 $$ fold (\_ _ -> ()) ()
не протекает, поэтому что-то в progress
действительно кажется проблемой. Я не вижу что.
ОБНОВЛЕНИЕ: утечка происходит только с ghci! Если я компилирую бинарный файл и запускаю его, утечки нет (я должен был проверить это раньше...)
Ответ 2
Я думаю, что ответ тома правильный, я начинаю его как отдельный ответ, так как он, вероятно, введет какое-то новое обсуждение (и потому что это слишком долго для простого комментария). В моем тестировании замена print b
в примере Tom на return ()
избавляет от утечки памяти. Это заставило меня думать, что проблема на самом деле с print
, а не с conduit
. Чтобы проверить эту теорию, я написал простую вспомогательную функцию в C (помещенную в helper.c):
#include <stdio.h>
void helper(int c)
{
printf("%d\n", c);
}
Затем я импортировал эту функцию в код на Haskell:
foreign import ccall "helper" helper :: Int -> IO ()
и я заменил звонок на print
звонком на helper
. Вывод из программы идентичен, но я показываю отсутствие утечек и максимальную резидентность 32 КБ против 62 КБ (я также изменил код, чтобы он останавливался на 10 м записи для лучшего сравнения).
Я вижу подобное поведение, когда полностью отключаю канал, например:
main :: IO ()
main = forM_ [1..10000000] $ \i ->
when (i 'mod' 100000 == 0) (helper i)
Однако я не уверен, что это действительно ошибка в print
или Handle
. Мое тестирование никогда не показывало утечку, достигающую сколько-нибудь существенного использования памяти, поэтому могло случиться так, что буфер увеличивается до предела. Мне нужно было провести дополнительные исследования, чтобы лучше понять это, но я сначала хотел посмотреть, соответствует ли этот анализ тому, что видят другие.
Ответ 3
Я знаю это два года спустя, но я подозреваю, что происходит то, что полная лень поднимает часть тела, ожидаемого до самого ожидания, и это вызывает утечку пространства. Он похож на случай в разделе "Увеличение общего доступа" в m y блоге на эту тему.