Утечка пространства вверх по DP в Haskell
Извиняюсь, если это слишком конкретно, я новичок здесь и не совсем уверен, что является разумным. Я часами бьюсь над этой проблемой, и мне нечего показать. Следующий код - моя реализация задачи конкурентного программирования.
module Main where
import Data.List (foldl', groupBy)
import Debug.Trace
type Case = (Int, [(Int, Int)])
type Soln = Int
main = interact handle
handle :: String -> String
handle = fmt . solve . parse
fmt :: Soln -> String
fmt s = (show s) ++ "\n"
parse :: String -> Case
parse s = (l, fs)
where
(l:_:fs') = (map read) $ words s
fs = pairs fs'
pairs :: [a] -> [(a, a)]
pairs [] = []
pairs (a:b:s) = (a, b):(pairs s)
solve :: Case -> Soln
solve c@(l, fs) = last $ foldl' run [0..l] f
where
f = concat $ map rep $ map combine $ groupBy samev fs
samev a b = (snd a) == (snd b)
combine a = (sum $ map fst $ a, snd $ head $ a)
rep (n, v) = replicate (min n (l `div` v)) v
run :: [Int] -> Int -> [Int]
run b v = (take v b) ++ (zipWith min b (drop v b))
-- run b v = (take v b) ++ (zipMin b (drop v b))
zipMin :: [Int] -> [Int] -> [Int]
zipMin [] _ = []
zipMin _ [] = []
zipMin (a:as) (b:bs) = (min a b):(zipMin as bs)
Намерение состоит в том, что это работает как восходящее динамическое программирующее решение, генерирующее каждую строку таблицы DP из предыдущей с использованием сгиба в execute Теоретически GHC должен иметь возможность оптимизировать все старые строки таблицы. Тем не менее, запуск этой программы на умеренно большом входе с примерно l = 2000
а также length f = 5000
Я получаю это:
> time ./E < E.in
0
1.98user 0.12system 0:02.10elapsed 99%CPU (0avgtext+0avgdata 878488maxresident)k
0inputs+0outputs (0major+219024minor)pagefaults 0swaps
Это использует 878 МБ памяти на пике! Таблица, которую я генерирую, составляет всего 10000 Ints, и теоретически мне нужна только одна строка за раз! Кажется очевидным, что это какая-то утечка грома или другая утечка пространства. Профилирование показывает, что run
потребляет 99% общего времени выполнения и памяти. Копание далее показало, что 98% этого было в zipWith
вызов. Интересно, что замена звонка на zipWith min
с моим обычаем zipMin
Функция дает значительное улучшение:
> time ./E < E.in
0
1.39user 0.08system 0:01.48elapsed 99%CPU (0avgtext+0avgdata 531400maxresident)k
0inputs+0outputs (0major+132239minor)pagefaults 0swaps
Это всего лишь 70% времени работы и 60% памяти! Я пробовал все виды, чтобы сделать эту работу. я знаю (++)
это вообще плохая идея, поэтому я заменил списки в run
с Data.Sequence.Seq Int
... и он стал медленнее и использовал больше памяти. У меня нет особого опыта работы с Хаскеллом, но я нахожусь в конце моего остроумия здесь. Я уверен, что ответ на эту проблему существует где-то на SO, но я слишком новичок в Haskell, чтобы найти его, кажется.
Любая помощь, которую вы можете предложить, очень ценится. Я хотел бы объяснить, что именно я сделал неправильно, как диагностировать это в будущем и как это исправить.
Заранее спасибо.
РЕДАКТИРОВАТЬ:
После следования превосходному совету Стивена и замены моих списков несвязанными векторами производительность... ну... значительно улучшилась:
> time ./E < E.in
0
0.01user 0.00system 0:00.02elapsed 80%CPU (0avgtext+0avgdata 5000maxresident)k
24inputs+0outputs (0major+512minor)pagefaults 0swaps
1 ответ
Итак, используя foldl'
Вы убедились, что аккумулятор будет в WHNF. Размещение списка в WHNF оценивает только первый элемент списка. Остальная часть списка существует в виде thunk, и будет передаваться как thunk для последующих вызовов вашей карты. Поскольку вас интересуют сразу несколько позиций в списке (то есть вы отбрасываете некоторые его части в zipWith
) большие части списков сохраняются от предыдущих итераций.
Структура, которая вам нужна, - это распакованный вектор. Распакованный вектор гарантирует, что все максимально строго и будет работать в гораздо меньшем объеме памяти.