Подтвердить что ты не робот

Почему складывающиеся события и поведение используют так много памяти?

В настоящее время я изучаю возможность использования базовых контейнеров, чтобы обеспечить дополнительную структуру 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 не должно создавать столько памяти для мусора. Любые идеи?

4b9b3361