Итерация рандомизированного алгоритма в фиксированном пространстве и линейное время
Я задавал один и тот же вопрос один раз. Теперь я буду более конкретным. Цель состоит в том, чтобы изучить икону Haskell для написания итеративных алгоритмов с моноданными результатами. В частности, это может быть полезно для реализации всех видов рандомизированных алгоритмов, таких как генетические алгоритмы и т.п.
Я написал примерную программу, которая проявляет мою проблему с такими алгоритмами в Haskell. Его полный источник находится на hpaste.
Ключевым моментом является случайное обновление элемента (таким образом, результат находится в State StdGen
или какой-либо другой монаде):
type RMonad = State StdGen
-- An example of random iteration step: one-dimensional random walk.
randStep :: (Num a) => a -> RMonad a
randStep x = do
rnd <- get
let (goRight,rnd') = random rnd :: (Bool, StdGen)
put rnd'
if goRight
then return (x+1)
else return (x-1)
И тогда нужно обновить многие элементы и повторить процесс много, много раз. И вот проблема. Поскольку каждый шаг - действие монады (:: a -> m a
), повторяющееся много раз, важно эффективно выполнять такие действия (быстро забывая о предыдущем шаге). Из того, что я узнал из своего предыдущего вопроса (Составление действий монады со сгибами), seq
и deepseq
помогают многому составить монадические действия. Поэтому я:
-- Strict (?) iteration.
iterateM' :: (NFData a, Monad m) => Int -> (a -> m a) -> a -> m a
iterateM' 0 _ x = return $!! x
iterateM' n f x = (f $!! x) >>= iterateM' (n-1) f
-- Deeply stict function application.
($!!) :: (NFData a) => (a -> b) -> a -> b
f $!! x = x `deepseq` f x
Это, безусловно, лучше ленивой композиции. К сожалению, этого недостаточно.
-- main seems to run in O(size*iters^2) time...
main :: IO ()
main = do
(size:iters:_) <- liftM (map read) getArgs
let start = take size $ repeat 0
rnd <- getStdGen
let end = flip evalState rnd $ iterateM' iters (mapM randStep) start
putStr . unlines $ histogram "%.2g" end 13
Когда я измерял время, необходимое для завершения этой программы, кажется, что он похож на O (N ^ 2) относительно количества итераций (распределение памяти представляется приемлемым). Этот профиль должен быть плоским и постоянным для линейной асимптотики:
квадратное время на обновление http://i29.tinypic.com/i59blv.png
И вот так выглядит профиль кучи:
профиль кучи с -hc http://i30.tinypic.com/124a8fc.png
Я предполагаю, что такая программа должна работать с очень скромными требованиями к памяти, и она должна занимать время пропорционально количеству итераций. Как я могу достичь этого в Haskell?
Полный исходный источник примера здесь.
Ответы
Ответ 1
Некоторые вещи, которые следует учитывать:
Для сырой тотальной производительности напишите пользовательскую монаду штата, например:
import System.Random.Mersenne.Pure64
data R a = R !a {-# UNPACK #-}!PureMT
-- | The RMonad is just a specific instance of the State monad where the
-- state is just the PureMT PRNG state.
--
-- * Specialized to a known state type
--
newtype RMonad a = S { runState :: PureMT -> R a }
instance Monad RMonad where
{-# INLINE return #-}
return a = S $ \s -> R a s
{-# INLINE (>>=) #-}
m >>= k = S $ \s -> case runState m s of
R a s' -> runState (k a) s'
{-# INLINE (>>) #-}
m >> k = S $ \s -> case runState m s of
R _ s' -> runState k s'
-- | Run function for the Rmonad.
runRmonad :: RMonad a -> PureMT -> R a
runRmonad (S m) s = m s
evalRmonad :: RMonad a -> PureMT -> a
evalRmonad r s = case runRmonad r s of R x _ -> x
-- An example of random iteration step: one-dimensional random walk.
randStep :: (Num a) => a -> RMonad a
randStep x = S $ \s -> case randomInt s of
(n, s') | n < 0 -> R (x+1) s'
| otherwise -> R (x-1) s'
Так: http://hpaste.org/fastcgi/hpaste.fcgi/view?id=27414#a27414
Что работает в постоянном пространстве (по модулю [Double]
, которое вы создаете), и в 8 раз быстрее, чем ваш оригинал.
Использование специализированной государственной монады с локальной дефиницией также значительно превосходит Control.Monad.Strict.
Здесь выглядит куча с теми же параметрами, что и вы:
![Custom state monad alt text]()
Обратите внимание, что он примерно в 10 раз быстрее и использует 1/5 пробел. Большая красная вещь - ваш список удваиваемых мест.
Вдохновленный вашим вопросом, я захватил образец PureMT в новом пакете: monad-mersenne-random, и теперь ваша программа станет следующей:
Другое изменение, которое я сделал, это преобразование work/wrapper iterateM, позволяющее ему быть вложенным:
{-# INLINE iterateM #-}
iterateM n f x = go n x
where
go 0 !x = return x
go n !x = f x >>= go (n-1)
В целом, это приводит к вашему коду, с K = 500, N = 30k
- Оригинал: 62.0s
- Новое: 0.28s
Итак, быстрее 220 раз.
Куча тоже немного лучше, теперь iterateM unboxes.
![alt text]()
Ответ 2
Импортировать Control.Monad.State.Strict вместо Control.Monad.State дает значительное улучшение производительности. Не уверен, что вы ищете с точки зрения асимптотики, но это может привести вас туда.
Кроме того, вы получаете увеличение производительности за счет замены iterateM и mapM, чтобы вы не продолжали перемещаться по списку, вам не нужно удерживать головку списка, и вам не нужно через этот список, но просто заставляйте отдельные результаты. То есть:.
let end = flip evalState rnd $ mapM (iterateM iters randStep) start
Если вы это сделаете, вы можете изменить iterateM также более идиоматично:
iterateM 0 _ x = return x
iterateM n f !x = f x >>= iterateM (n-1) f
Для этого, конечно, требуется расширение языка шаблонов.
Ответ 3
Это, вероятно, небольшая точка по сравнению с другими ответами, но верно ли ваша функция ($!!)?
Вы определяете
($!!) :: (NFData a) => (a -> b) -> a -> b
f $!! x = x `deepseq` f x
Это будет полностью оценивать аргумент, однако результат функции не обязательно будет оцениваться вообще. Если вы хотите, чтобы оператор $!!
применил эту функцию и полностью оценил результат, я думаю, что это должно быть:
($!!) :: (NFData b) => (a -> b) -> a -> b
f $!! x = let y = f x in y `deepseq` y