Haskell: Эффективная итерационная карта с добавленным шумом
Мне интересно, как улучшить временные характеристики добавления белого шума к логистической карте? Шум допускается добавлять только после расчета значений (так как это итерационная карта).
module Generating
import System.Random (Random,randomR,random,mkStdGen,StdGen)
import Data.Random.Normal (mkNormals,normal)
import qualified Data.Vector.Unboxed as U
import Control.Monad.State
genR :: (Random a, Fractional a) => Int -> (a, StdGen)
genR x = randomR (0,1.0) (mkStdGen x)
new ::Double-> Double ->Int -> (Double,Int) -> U.Vector (Double,Int)
new skal r n = U.unfoldrN n go
where
go (x0,g0) = let !eins= (1.0-x0)
!x=x0 `seq` eins `seq` r*x0*eins
!g=g0+1
!noise= skal*(fst $ genR g)
in Just ((x0+noise,g0),(x,g))
fs :: (t, t1) -> t
fs (x,y)=x
first :: U.Vector (Double,Int)->U.Vector Double
first =U.map (\(x,y)->x)
Как видите, я на самом деле хочу только первое значение кортежа, но генератор должен быть обновлен.
Какие-либо предложения? Может быть, государственные монады?
1 ответ
tl;dr: не пытайтесь оптимизировать программу на Haskell без использования профилирования и бенчмаркинга. Добавление случайных восклицательных знаков и seq
С почти никогда не будет работать. Большая проблема здесь в том, что StdGen
это невероятно медленный генератор случайных чисел, который полностью доминирует во времени выполнения вашей программы. Вы должны заменить его, чтобы добиться значительного прогресса.
Вот более длинный ответ: хорошим первым шагом является установка библиотеки для бенчмаркинга, например criterion
и напишите контрольный пример:
import Criterion.Main
...your program above...
vect1 :: (Double, Int) -> U.Vector Double
vect1 = first . new 0.5 1 10000
main = defaultMain [
bench "vect1" $ nf vect1 (0,1)
]
В моем случае результаты выглядят так:
benchmarking vect1
time 8.097 ms (8.071 ms .. 8.125 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 8.140 ms (8.124 ms .. 8.162 ms)
std dev 52.90 μs (36.32 μs .. 91.72 μs)
поэтому у нас есть около 8 миллисекунд на цикл для генерации вектора из 10000 элементов.
Теперь давайте избавимся от всех ударов, seq
s и промежуточные вычисления, которые вы добавили, чтобы попытаться ускорить процесс:
new :: Double-> Double -> Int -> (Double, Int) -> U.Vector (Double,Int)
new skal r n = U.unfoldrN n go
where
go (x0,g0) = let x = r * x0 * (1-x0)
g = g0 + 1
noise = skal * (fst $ genR g)
in Just ((x0+noise, g0), (x,g))
Повторяю, вот результаты:
time 8.195 ms (8.168 ms .. 8.235 ms)
Ах, они вообще не имели никакого эффекта. Рад, что мы от них избавились.
Теперь стоит отметить, что unfoldrN
несет с собой аккумулятор для вас, который держится за ваш g
, Вам также не нужно включать g
в результате, если вы собираетесь все равно выбросить, мы можем упростить new
чтобы:
new :: Double-> Double -> Int -> (Double, Int) -> U.Vector Double
new skal r n = U.unfoldrN n go
where
go (x0,g0) = let x = r * x0 * (1-x0)
g = g0 + 1
noise = skal * (fst $ genR g)
in Just (x0+noise, (x,g))
и бросить first
вызов из определения vect1
:
vect1 :: (Double, Int) -> U.Vector Double
vect1 = new 0.5 1 10000
Это дает:
time 8.289 ms (8.238 ms .. 8.373 ms)
так что это не имело значения. Несомненно, компилятор смог оптимизировать ненужные лишние Double
в любом случае, поэтому изменение кода не дало никаких результатов.
Более серьезная проблема с алгоритмом состоит в том, что он использует генераторы очень странным образом. StdGen
предназначен для посева и последующего повторного использования для генерации нескольких случайных чисел, а не для генерации свежей из начального числа на основе счетчика. Мы действительно должны переписать new
правильно использовать генератор:
new :: Double-> Double -> Int -> (Double, Int) -> U.Vector Double
new skal r n (x0, seed) = U.unfoldrN n go (x0, g0)
where
g0 = mkStdGen seed -- create initial generator from seed
go (x0,g0) = let (eps, g) = randomR (0, 1.0) g0 -- use generator properly
x = r * x0 * (1-x0)
noise = skal * eps
in Just (x0 + noise, (x, g))
хотя опять же, это практически не имеет значения для нашего времени тестирования. Я признаю, что это удивило меня. Я думал, что это будет иметь значительный эффект. Хорошо, что я сравнивал эти изменения, поэтому у меня были реальные объективные доказательства эффекта (или отсутствия эффекта) этого изменения!
Теперь, похоже, пришло время описать нашу программу и посмотреть, на что она тратит свое время.
$ stack ghc -- -prof -fprof-auto -O2 Generating.hs
$ ./Generating -n 100 +RTS -p # run 100 iterations
Если вы посмотрите на Generating.prof
файл, который выводится, вы увидите, что большая часть времени проводится в System.Random
, вот так:
COST CENTRE MODULE SRC %time %alloc
randomR System.Random System/Random.hs:409:3-27 21.7 24.0
stdNext System.Random System/Random.hs:(518,1)-(528,64) 15.4 16.6
randomIvalInteger System.Random System/Random.hs:(468,1)-(489,76) 12.2 12.0
randomIvalInteger.f System.Random System/Random.hs:(486,8)-(489,76) 11.0 4.8
randomIvalInteger.f.v' System.Random System/Random.hs:489:25-76 7.0 8.6
Оказывается, что стандартный генератор случайных чисел на Haskell работает ужасно медленно, и нам нужно заменить его чем-то более быстрым, чтобы добиться большего прогресса.
mersenne-random-pure64
Пакет обеспечивает быструю реализацию Mersenne Twister, которая производит случайные числа высокого качества, и мы можем переписать new
использовать это. Обратите внимание, что randomDouble
возвращает равномерное случайное число в интервале [0,1)
:
import System.Random.Mersenne.Pure64
new :: Double-> Double -> Int -> (Double, Int) -> U.Vector Double
new skal r n (x0, seed) = U.unfoldrN n go (x0, g0)
where
g0 = pureMT (fromIntegral seed)
go (x0,g0) = let (eps, g) = randomDouble g0
x = r * x0 * (1-x0)
noise = skal * eps
in Just (x0 + noise, (x, g))
Повторный бенчмаркинг (перекомпилированный без профилирования) дает:
benchmarking vect1
time 106.7 μs (106.4 μs .. 107.0 μs)
1.000 R² (1.000 R² .. 1.000 R²)
mean 107.1 μs (106.7 μs .. 107.7 μs)
std dev 1.415 μs (842.3 ns .. 2.377 μs)
Обратите внимание, что это 107 микросекунд, так что это примерно в 75 раз быстрее.
На этом я бы остановился, но если вы решите продолжить оптимизацию, убедитесь, что вы часто обращаетесь к результатам профилирования и бенчмаркинга, чтобы убедиться, что ваши изменения дают о себе знать.
Я настоятельно рекомендую Googling для "профилирования программ на haskell" и для "критериальной" библиотеки, а также уделю некоторое время изучению того, как использовать эти инструменты.
Для справки, окончательная программа:
import Criterion.Main
import qualified Data.Vector.Unboxed as U
import System.Random.Mersenne.Pure64
new :: Double-> Double -> Int -> (Double, Int) -> U.Vector Double
new skal r n (x0, seed) = U.unfoldrN n go (x0, g0)
where
g0 = pureMT (fromIntegral seed)
go (x0,g0) = let (eps, g) = randomDouble g0
x = r * x0 * (1-x0)
noise = skal * eps
in Just (x0 + noise, (x, g))
vect1 :: (Double, Int) -> U.Vector Double
vect1 = new 0.5 1 10000
main = defaultMain [
bench "vect1" $ nf vect1 (0,1)
]