В настоящее время я изучаю возможность использования базовых контейнеров, чтобы обеспечить дополнительную структуру FRP-сетей и тем самым упростить создание более сложных сетей событий.
Примечание: Я использую ordrea, но имел ту же проблему с reactive-banana, поэтому я предполагаю, что эта проблема не относится к выбранной реализации frp.
В этом специальном случае я использую простой Matrix
для хранения Events
:
newtype Matrix (w :: Nat) (h :: Nat) v a where
Matrix :: Vector a -> Matrix w h v a
-- deriving instances: Functor, Foldable, Traversable, Applicative
Matrix
- это всего лишь тонкая оболочка вокруг Data.Vector
, и большинство функций, которые я буду использовать, в основном такие же, как соответствующие Vector
. Заметным исключением является индексирование, но это должно быть самоочевидным.
С этим я могу определить матрицы событий типа Matrix 10 10 (Event Double)
и определить основные алгоритмы свертки на этом:
applyStencil :: (KnownNat w, KnownNat h, KnownNat w', KnownNat h')
=> M.Matrix w' h' (a -> c)
-> M.Matrix w h (Event a)
-> M.Matrix w h (Event c)
applyStencil s m = M.generate stencil
where stencil x y = fold $ M.imap (sub x y) s
sub x0 y0 x y g = g <$> M.clampedIndex m (x0 - halfW + x) (y0 - halfH + y)
halfW = M.width s `div` 2
halfH = M.height s `div` 2
Примечания:
-
M.generate :: (Int -> Int -> a) -> M.Matrix w h a
иM.imap :: (Int -> Int -> a -> b) -> M.Matrix w h a -> M.Matrix w h b
- это просто обертки вокруг
Vector.generate
иVector.imap
соответственно. -
M.clampedIndex
фиксирует индексы в границах матрицы. -
Event
- это экземплярMonoid
, поэтому возможно толькоfold
Matrix w' h' (Event c)
, возвращаемоеM.imap (sub x y) s
.
У меня есть настройка примерно так:
let network = do
-- inputs triggered from external events
let inputs :: M.Matrix 128 128 (Event Double)
-- stencil used:
let stencil :: M.Matrix 3 3 (Double -> Double)
stencil = fmap ((*) . (/16)) $ M.fromList [1,2,1,2,4,2,1,2,1]
-- convolute matrix by applying stencil
let convoluted = applyStencil stencil inputs
-- collect events in order to display them later
-- type: M.Matrix 128 128 (Behavior [Double])
let behaviors = fmap eventToBehavior convoluted
-- now there is a neat trick you can play because Matrix
-- is Traversable and Behaviors are Applicative:
-- type: Behavior (Matrix 128 128 [Double])
return $ Data.Traversable.sequenceA behaviors
Используя что-то подобное, я запускаю ~ 15kEvents/s без проблем и большого количества запаса в этом отношении.
Проблема заключается в том, что, как только я пробовал сеть, я могу получить только два образца в секунду:
main :: IO ()
main = do
-- initialize the network
sample <- start network
forever $ do
-- not all of the 128*128 inputs are triggered each "frame"
triggerInputs
-- sample the network
mat <- sample
-- display the matrix somehow (actually with gloss)
displayMatrix mat
До сих пор я сделал следующие замечания:
- Профилирование говорит мне, что производительность очень низкая (4% -8%)
- Большую часть времени тратит сборщик мусора в Gen 1 (~ 95%)
-
Data.Matrix.foldMap
(т.е.fold
) выделяет большую часть памяти (~ 45%, согласно-p
) -
Когда я все еще работал с реактивным бананом, Генрих Апфельмус рекомендовал, чтобы траверсы на основе дерева лучше подходят для поведения ¹. Я пробовал это для
sequenceA
,fold
иtraverse
без успеха. - Я подозревал, что оболочка newtype препятствует правилам фьюжн векторов для ². Это, скорее всего, не преступник.
В этот момент я потратил большую часть недели на поиск решения этой проблемы. Интуитивно я бы сказал, что выборка должна быть намного быстрее, и foldMap
не должно создавать столько памяти для мусора. Любые идеи?