Профилирование программы Haskell
У меня есть фрагмент кода, который многократно отображает из распределения вероятности с помощью sequence
. Морально, он делает что-то вроде этого:
sampleMean :: MonadRandom m => Int -> m Float -> m Float
sampleMean n dist = do
xs <- sequence (replicate n dist)
return (sum xs)
За исключением того, что это немного сложнее. Фактический код, который меня интересует, это функция likelihoodWeighting
в это репозиторий Github.
Я заметил, что время выполнения нелинейно изменяется с помощью n
. В частности, раз n
превышает определенное значение, он достигает предела памяти, и время работы взрывается. Я не уверен, но я думаю, что это потому, что sequence
создает длинный список thunks, которые не получают оценку до вызова sum
.
Как только я пройду около 100 000 образцов, программа замедлит сканирование. Я бы хотел оптимизировать это (я чувствую, что 10 миллионов образцов не должны быть проблемой), поэтому я решил профилировать его, но у меня есть небольшая проблема с пониманием вывода профилировщика.
Профилирование
Я создал короткий исполняемый файл в файле main.hs
, который запускает мою функцию с 100 000 образцов. Здесь вывод из
$ ghc -O2 -rtsopts main.hs
$ ./main +RTS -s
Первое, что я заметил - он выделяет около 1,5 Гбайт кучи и тратит 60% своего времени на сбор мусора. Это обычно указывает на слишком большую лень?
1,377,538,232 bytes allocated in the heap
1,195,050,032 bytes copied during GC
169,411,368 bytes maximum residency (12 sample(s))
7,360,232 bytes maximum slop
423 MB total memory in use (0 MB lost due to fragmentation)
Generation 0: 2574 collections, 0 parallel, 2.40s, 2.43s elapsed
Generation 1: 12 collections, 0 parallel, 1.07s, 1.28s elapsed
INIT time 0.00s ( 0.00s elapsed)
MUT time 1.92s ( 1.94s elapsed)
GC time 3.47s ( 3.70s elapsed)
RP time 0.00s ( 0.00s elapsed)
PROF time 0.23s ( 0.23s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 5.63s ( 5.87s elapsed)
%GC time 61.8% (63.1% elapsed)
Alloc rate 716,368,278 bytes per MUT second
Productivity 34.2% of total user, 32.7% of total elapsed
Вот результаты от
$ ./main +RTS -p
В первый раз, когда я это запустил, выяснилось, что одна функция вызывается многократно, и оказалось, что я мог ее мемуаровать, что ускорило ситуацию в 2 раза. Это не решило утечку пространства, однако.
COST CENTRE MODULE no. entries %time %alloc %time %alloc
MAIN MAIN 1 0 0.0 0.0 100.0 100.0
main Main 434 4 0.0 0.0 100.0 100.0
likelihoodWeighting AI.Probability.Bayes 445 1 0.0 0.3 100.0 100.0
distributionLW AI.Probability.Bayes 448 1 0.0 2.6 0.0 2.6
getSampleLW AI.Probability.Bayes 446 100000 20.0 50.4 100.0 97.1
bnProb AI.Probability.Bayes 458 400000 0.0 0.0 0.0 0.0
bnCond AI.Probability.Bayes 457 400000 6.7 0.8 6.7 0.8
bnVals AI.Probability.Bayes 455 400000 20.0 6.3 26.7 7.1
bnParents AI.Probability.Bayes 456 400000 6.7 0.8 6.7 0.8
bnSubRef AI.Probability.Bayes 454 800000 13.3 13.5 13.3 13.5
weightedSample AI.Probability.Bayes 447 100000 26.7 23.9 33.3 25.3
bnProb AI.Probability.Bayes 453 100000 0.0 0.0 0.0 0.0
bnCond AI.Probability.Bayes 452 100000 0.0 0.2 0.0 0.2
bnVals AI.Probability.Bayes 450 100000 0.0 0.3 6.7 0.5
bnParents AI.Probability.Bayes 451 100000 6.7 0.2 6.7 0.2
bnSubRef AI.Probability.Bayes 449 200000 0.0 0.7 0.0 0.7
Здесь находится куча профиля. Я не знаю, почему он утверждает, что время выполнения составляет 1,8 секунды - этот прогон занял около 6 секунд.
![enter image description here]()
Может ли кто-нибудь помочь мне интерпретировать вывод профилировщика, т.е. определить, где находится узкое место, и предоставить предложения о том, как ускорить работу?
Ответы
Ответ 1
Огромное улучшение уже достигнуто за счет включения предложения JohnL использования foldM
в likelihoodWeighting
. Это уменьшило использование памяти примерно в десять раз здесь и значительно сократило время GC почти или фактически незначительно.
Профилирование с текущим источником дает
probabilityIO AI.Util.Util 26.1 42.4 413 290400000
weightedSample.go AI.Probability.Bayes 16.1 19.1 255 131200080
bnParents AI.Probability.Bayes 10.8 1.2 171 8000384
bnVals AI.Probability.Bayes 10.4 7.8 164 53603072
bnCond AI.Probability.Bayes 7.9 1.2 125 8000384
ndSubRef AI.Util.Array 4.8 9.2 76 63204112
bnSubRef AI.Probability.Bayes 4.7 8.1 75 55203072
likelihoodWeighting.func AI.Probability.Bayes 3.3 2.8 53 19195128
%! AI.Util.Util 3.3 0.5 53 3200000
bnProb AI.Probability.Bayes 2.5 0.0 40 16
bnProb.p AI.Probability.Bayes 2.5 3.5 40 24001152
likelihoodWeighting AI.Probability.Bayes 2.5 2.9 39 20000264
likelihoodWeighting.func.x AI.Probability.Bayes 2.3 0.2 37 1600000
и использование памяти в 13 МБ, о которых сообщается -s
, ~ 5 Мбайт. Это уже не так уж плохо.
Тем не менее, остаются некоторые моменты, которые мы можем улучшить. Во-первых, относительно небольшая вещь, в большой схеме, AI.UTIl.Array.ndSubRef
:
ndSubRef :: [Int] -> Int
ndSubRef ns = sum $ zipWith (*) (reverse ns) (map (2^) [0..])
Отмена списка, а отображение (2^)
по другому списку неэффективно, лучше
ndSubRef = L.foldl' (\a d -> 2*a + d) 0
которому не нужно хранить весь список в памяти (возможно, это не так уж важно, поскольку списки будут короткими), как это делает вспять, и не нужно выделять второй список. Снижение распределения заметно, около 10%, и эта часть работает значительно быстрее,
ndSubRef AI.Util.Array 1.7 1.3 24 8000384
в профиле модифицированного прогона, но поскольку он занимает лишь небольшую часть общего времени, общее воздействие мало. Есть потенциально большая рыба, чтобы жарить в weightedSample
и likelihoodWeighting
.
Добавьте в строчку weightedSample
немного строгости, чтобы увидеть, как это меняет вещи:
weightedSample :: Ord e => BayesNet e -> [(e,Bool)] -> IO (Map e Bool, Prob)
weightedSample bn fixed =
go 1.0 (M.fromList fixed) (bnVars bn)
where
go w assignment [] = return (assignment, w)
go w assignment (v:vs) = if v `elem` vars
then
let w' = w * bnProb bn assignment (v, fixed %! v)
in go w' assignment vs
else do
let p = bnProb bn assignment (v,True)
x <- probabilityIO p
go w (M.insert v x assignment) vs
vars = map fst fixed
Параметр веса go
никогда не принудительно и не является параметром присваивания, таким образом, они могут создавать громы. Разрешите включение {-# LANGUAGE BangPatterns #-}
и принудительные обновления немедленно вступят в силу, также оцените p
, прежде чем передать его в probabilityIO
:
go w assignment (v:vs) = if v `elem` vars
then
let !w' = w * bnProb bn assignment (v, fixed %! v)
in go w' assignment vs
else do
let !p = bnProb bn assignment (v,True)
x <- probabilityIO p
let !assignment' = M.insert v x assignment
go w assignment' vs
Это приводит к дальнейшему сокращению распределения (~ 9%) и небольшому ускорению (~% 13%), но общий объем использования памяти и максимальное место жительства сильно не изменились.
Я не вижу ничего другого, что может измениться там, поэтому посмотрим на likelihoodWeighting
:
func m _ = do
(a, w) <- weightedSample bn fixed
let x = a ! e
return $! x `seq` w `seq` M.adjust (+w) x m
В последней строке сначала w
уже оценивается в weightedSample
, поэтому нам не нужно seq
здесь, для оценки обновленной карты требуется ключ x
, поэтому seq
ing, что тоже не нужно. Плохая вещь на этой строке M.adjust
. adjust
не имеет возможности заставлять результат обновленной функции, так что строит thunks в значениях карты. Вы можете принудительно оценивать удары, просматривая измененное значение и заставляя это, но Data.Map
обеспечивает гораздо более удобный способ, так как ключ, на котором обновляется карта, гарантированно присутствует, insertWith'
:
func !m _ = do
(a, w) <- weightedSample bn fixed
let x = a ! e
return (M.insertWith' (+) x w m)
(Примечание: GHC лучше оптимизируется с помощью шаблона bang на m
, чем с return $! ...
здесь). Это немного уменьшает общее распределение и не меняет время работы, но оказывает значительное влияние на используемую общую память и максимальное место жительства:
934,566,488 bytes allocated in the heap
1,441,744 bytes copied during GC
68,112 bytes maximum residency (1 sample(s))
23,272 bytes maximum slop
1 MB total memory in use (0 MB lost due to fragmentation)
Наибольшее улучшение времени работы было бы, если избежать randomIO
, используемый StdGen
будет очень медленным.
Я удивляюсь, сколько времени выполняют функции bn*
, но не вижу в них очевидной неэффективности.
Ответ 2
У меня проблемы с перевариванием этих профилей, но я получил свою задницу раньше, потому что MonadRandom
в Hackage строг. Создание ленивой версии MonadRandom
затруднило проблемы с памятью.
Мой коллега еще не получил разрешения на выпуск кода, но я положил Control.Monad.LazyRandom
в онлайн pastebin. Или, если вы хотите увидеть некоторые отрывки, которые объясняют полностью ленивый случайный поиск, в том числе бесконечные списки случайных вычислений, посмотрите Отчет о работе: Haskell in Computational Biology.
Ответ 3
Я собрал очень элементарный пример, размещенный здесь: http://hpaste.org/71919. Я не уверен, что это похоже на ваш пример.. просто очень минимальная вещь, которая, казалось, сработала.
Компиляция с -prof
и -fprof-auto
и работа с 100000 итерациями дала следующую главу профилирующего вывода (помилуй мои номера строк):
8 COST CENTRE MODULE %time %alloc
9
10 sample AI.Util.ProbDist 31.5 36.6
11 bnParents AI.Probability.Bayes 23.2 0.0
12 bnRank AI.Probability.Bayes 10.7 23.7
13 weightedSample.go AI.Probability.Bayes 9.6 13.4
14 bnVars AI.Probability.Bayes 8.6 16.2
15 likelihoodWeighting AI.Probability.Bayes 3.8 4.2
16 likelihoodWeighting.getSample AI.Probability.Bayes 2.1 0.7
17 sample.cumulative AI.Util.ProbDist 1.7 2.1
18 bnCond AI.Probability.Bayes 1.6 0.0
19 bnRank.ps AI.Probability.Bayes 1.1 0.0
И вот сводная статистика:
1,433,944,752 bytes allocated in the heap
1,016,435,800 bytes copied during GC
176,719,648 bytes maximum residency (11 sample(s))
1,900,232 bytes maximum slop
400 MB total memory in use (0 MB lost due to fragmentation)
INIT time 0.00s ( 0.00s elapsed)
MUT time 1.40s ( 1.41s elapsed)
GC time 1.08s ( 1.24s elapsed)
Total time 2.47s ( 2.65s elapsed)
%GC time 43.6% (46.8% elapsed)
Alloc rate 1,026,674,336 bytes per MUT second
Productivity 56.4% of total user, 52.6% of total elapsed
Обратите внимание, что профайлер указал пальцем на sample
. Я принудительно использовал return
в этой функции, используя $!
, а затем следующую сводную статистику:
1,776,908,816 bytes allocated in the heap
165,232,656 bytes copied during GC
34,963,136 bytes maximum residency (7 sample(s))
483,192 bytes maximum slop
68 MB total memory in use (0 MB lost due to fragmentation)
INIT time 0.00s ( 0.00s elapsed)
MUT time 2.42s ( 2.44s elapsed)
GC time 0.21s ( 0.23s elapsed)
Total time 2.63s ( 2.68s elapsed)
%GC time 7.9% (8.8% elapsed)
Alloc rate 733,248,745 bytes per MUT second
Productivity 92.1% of total user, 90.4% of total elapsed
Гораздо более продуктивный с точки зрения GC, но не так сильно изменился в то время. Возможно, вам удастся продолжить повторение в этом профиле/настройке, чтобы нацелить ваши узкие места и повысить производительность.
Ответ 4
Я думаю, что ваш первоначальный диагноз верен, и я никогда не видел отчет о профилировании, который полезен после того, как эффекты памяти начнутся.
Проблема в том, что вы перебираете список дважды, один раз для sequence
и снова для sum
. В Haskell несколько переходов списка больших списков действительно, очень плохо для производительности. Решение, как правило, должно использовать некоторый тип складок, например foldM
. Ваша функция sampleMean
может быть записана как
{-# LANGUAGE BangPatterns #-}
sampleMean2 :: MonadRandom m => Int -> m Float -> m Float
sampleMean2 n dist = foldM (\(!a) mb -> liftM (+a) mb) 0 $ replicate n dist
например, перемещение списка только один раз.
Вы можете сделать то же самое с likelihoodWeighting
. Чтобы предотвратить торможение, важно убедиться, что аккумулятор в вашей функции сгиба имеет соответствующую строгость.