Как хранить очень большие элементы памяти без извлечения сборщика мусора?

В Haskell я создал вектор 1000000 IntMaps. Затем я использовал Gloss для рендеринга изображения таким образом, чтобы получить доступ к случайным intmaps из этого вектора.
То есть, я сохранил каждый из них в памяти. Сама функция рендеринга очень легкая, поэтому производительность должна быть хорошей.
Тем не менее, программа работала со скоростью 4 кадра в секунду. После профилирования я заметил, что 95% времени было потрачено на GC. Достаточно справедливо:
GC безумно сканирует мой вектор, хотя он никогда не меняется.

Есть ли способ рассказать GHC "это большое значение необходимо и не изменится - не пытайтесь собрать что-либо внутри него".

Изменить: нижеприведенной программы достаточно для репликации проблемы.

import qualified Data.IntMap as Map
import qualified Data.Vector as Vec
import Graphics.Gloss
import Graphics.Gloss.Interface.IO.Animate
import System.Random

main = do
    let size  = 10000000
    let gen i = Map.fromList $ zip [mod i 10..0] [0..mod i 10]
    let vec   = Vec.fromList $ map gen [0..size]
    let draw t = do 
            rnd <- randomIO :: IO Int
            let empty = Map.null $ vec Vec.! mod rnd size
            let rad   = if empty then 10 else 50
            return $ translate (20 * cos t) (20 * sin t) (circle rad)
    animateIO (InWindow "hi" (256,256) (1,1)) white draw

Получает случайную карту на огромном векторе и рисует вращающийся круг, радиус которого зависит от того, пустое ли оно. Несмотря на то, что эта логика очень проста, программа здесь работает примерно на 1 FPS.

Ответы

Ответ 1

блеск - вот преступник.

Во-первых, немного информации о сборщике мусора GHC. GHC использует (по умолчанию) коллективный сборщик мусора. Это означает, что куча состоит из нескольких областей памяти, называемых поколениями. Объекты выделяются в самое младшее поколение. Когда поколение заполняется, оно сканируется для живых объектов, а живые объекты копируются в следующее поколение, а затем сканируемое поколение помечено как пустое. Когда старое поколение становится полным, живые объекты вместо этого копируются в новую версию самого старого поколения.

Важным фактом, чтобы убрать это, является то, что GC только когда-либо рассматривает живые объекты. Мертвые объекты никогда не касаются. Это здорово, когда вы собираете поколения, которые в основном являются мусором, как это часто бывает у самого молодого поколения. Это не хорошо, если долговечные данные подвергаются много GC, поскольку они будут скопированы повторно. (Он также может быть несовместимым с теми, которые используются для управления памятью malloc/free-style, где выделение и освобождение являются довольно дорогими, но оставляя объекты, выделенные в течение длительного времени, не имеют прямых затрат.)

Теперь "гипотеза поколений" состоит в том, что большинство объектов либо недолговечны, либо долгоживут. Долгоживущие объекты быстро окажутся в старейшем поколении, так как они живы в каждой коллекции. Между тем, большинство короткоживущих объектов, которые выделяются, никогда не выживут в младшем поколении; только те, которые оживают, когда они будут собраны, будут продвигаться к следующему поколению. Точно так же большинство из тех короткоживущих объектов, которые получают повышение, не выживут в третьем поколении. В результате, самое старое поколение, в котором хранятся долгоживущие объекты, должно заполняться очень медленно, и его дорогие коллекции, которые должны копировать все долгоживущие объекты, должны встречаться редко.

Теперь все это действительно верно в вашей программе, за исключением одной проблемы:

    let displayFun backendRef = do
            -- extract the current time from the state
            timeS           <- animateSR `getsIORef` AN.stateAnimateTime

            -- call the user action to get the animation frame
            picture         <- frameOp (double2Float timeS)

            renderS         <- readIORef renderSR
            portS           <- viewStateViewPort <$> readIORef viewSR

            windowSize      <- getWindowDimensions backendRef

            -- render the frame
            displayPicture
                    windowSize
                    backColor
                    renderS
                    (viewPortScale portS)
                    (applyViewPortToPicture portS picture)

            -- perform GC every frame to try and avoid long pauses
            performGC

gloss сообщает GC собирать самое старое поколение в каждом кадре!

Это может быть хорошей идеей, если ожидается, что эти коллекции потребуют меньше времени, чем задержка между кадрами, но это явно не очень хорошая идея для вашей программы. Если вы удалите этот вызов performGC из блеска, ваша программа будет работать довольно быстро. Предположительно, если вы позволите ему работать достаточно долго, то старейшее поколение в конечном итоге заполнится, и вы получите задержку в несколько десятых секунды, поскольку GC скопирует все ваши долговечные данные, но это намного лучше, чем оплата этой стоимости каждый кадр.

Все, что сказано, есть билет # 9052 о добавлении стабильного поколения, которое также прекрасно подойдет вашим потребностям. См. Более подробную информацию.

Ответ 3

Чтобы добавить к Reid ответ, я нашел performMinorGC (добавлен в https://ghc.haskell.org/trac/ghc/ticket/8257), чтобы быть лучшим из обоих миров здесь,

Без какого-либо явного планирования GC я все равно получаю частые кадровые кадры, связанные с доставкой, по мере того, как питомник истощается. Но performGC действительно становится убийством по производительности, как только существует значительная долговременная память.

performMinorGC делает то, что мы хотим, игнорируя долгосрочную память и прогнозируя уборку мусора из каждого кадра - особенно если вы настроите -H и -A, чтобы мусор для каждого фрейма помещался в детскую.