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

Код становится медленнее по мере того, как распределяются больше массивов в коробке

Изменить: Оказывается, что вещи вообще (а не только операции массива /ref ) замедляют работу массивов, поэтому я думаю, что это может быть просто измерение увеличенного времени GC и может не быть как ни странно, как я думал. Но я действительно хотел бы узнать (и узнать, как узнать), что здесь происходит, и если есть способ смягчить этот эффект в коде, который создает множество небольших массивов. Исходный вопрос следует.


При исследовании некоторых странных результатов бенчмаркинга в библиотеке я наткнулся на какое-то поведение, которое я не понимаю, хотя это может быть действительно очевидно. Похоже, что время, затрачиваемое на многие операции (создание нового MutableArray, чтение или изменение IORef), увеличивается пропорционально количеству массивов в памяти.

Вот первый пример:

module Main
    where 

import Control.Monad
import qualified Data.Primitive as P
import Control.Concurrent
import Data.IORef
import Criterion.Main
import Control.Monad.Primitive(PrimState)

main = do 
  let n = 100000
  allTheArrays <- newIORef []
  defaultMain $
    [ bench "array creation" $ do
         newArr <- P.newArray 64 () :: IO (P.MutableArray (PrimState IO) ())
         atomicModifyIORef'  allTheArrays (\l-> (newArr:l,()))
    ]

Мы создаем новый массив и добавляем его в стек. Поскольку критерий делает больше выборок и стек растет, создание массива занимает больше времени, и это, кажется, растет линейно и регулярно:

slowdown

Более странно, читаются и записываются IORef, и мы можем видеть, что atomicModifyIORef' становится быстрее, поскольку больше массивов GC'd.

main = do 
  let n = 1000000
  arrs <- replicateM (n) $ (P.newArray 64 () :: IO (P.MutableArray (PrimState IO) ()))
  -- print $ length arrs -- THIS WORKS TO MAKE THINGS FASTER
  arrsRef <- newIORef arrs
  defaultMain $
    [ bench "atomic-mods of IORef" $
    -- nfIO $           -- OR THIS ALSO WORKS
        replicateM 1000 $
          atomicModifyIORef' arrsRef (\(a:as)-> (as,()))
    ]

enter image description here

Любая из двух строк, которые прокомментированы, избавляется от этого поведения, но я не уверен, почему (возможно, после того, как мы заставили позвоночник списка, элементы могут фактически собраться).

Вопросы

  • Что здесь происходит?
  • Ожидается ли поведение?
  • Есть ли способ избежать этого замедления?

Изменить. Я предполагаю, что это связано с тем, что GC занимает больше времени, но я хотел бы более точно понять, что происходит, особенно в первом эталоне.


Пример бонуса

Наконец, вот простая тестовая программа, которая может быть использована для предварительного выделения некоторого количества массивов и времени в виде группы atomicModifyIORef s. Это похоже на медленное поведение IORef.

import Control.Monad
import System.Environment

import qualified Data.Primitive as P
import Control.Concurrent
import Control.Concurrent.Chan
import Control.Concurrent.MVar
import Data.IORef
import Criterion.Main
import Control.Exception(evaluate)
import Control.Monad.Primitive(PrimState)

import qualified Data.Array.IO as IO
import qualified Data.Vector.Mutable as V

import System.CPUTime
import System.Mem(performGC)
import System.Environment
main :: IO ()
main = do
  [n] <- fmap (map read) getArgs
  arrs <- replicateM (n) $ (P.newArray 64 () :: IO (P.MutableArray (PrimState IO) ()))
  arrsRef <- newIORef arrs

  t0 <- getCPUTimeDouble

  cnt <- newIORef (0::Int)
  replicateM_ 1000000 $
    (atomicModifyIORef' cnt (\n-> (n+1,())) >>= evaluate)

  t1 <- getCPUTimeDouble

  -- make sure these stick around
  readIORef cnt >>= print
  readIORef arrsRef >>= (flip P.readArray 0 . head) >>= print

  putStrLn "The time:"
  print (t1 - t0)

Профиль кучи с -hy показывает в основном MUT_ARR_PTRS_CLEAN, что я не совсем понимаю.

Если вы хотите воспроизвести, вот файл cabal, который я использовал

name:                small-concurrency-benchmarks
version:             0.1.0.0       
build-type:          Simple
cabal-version:       >=1.10

executable small-concurrency-benchmarks
  main-is:             Main.hs 
  build-depends:       base >=4.6
                     , criterion
                     , primitive

  default-language:    Haskell2010
  ghc-options: -O2  -rtsopts

Изменить. Здесь еще одна тестовая программа, которая может использоваться для сравнения замедления с кучами одного и того же размера массивов vs [Integer]. Требуется некоторая пробная и корректировка ошибок n и наблюдение профилирования для получения сопоставимых прогонов.

main4 :: IO ()
main4= do
  [n] <- fmap (map read) getArgs
  let ns = [(1::Integer).. n]
  arrsRef <- newIORef ns
  print $ length ns

  t0 <- getCPUTimeDouble
  mapM (evaluate . sum) (tails [1.. 10000])
  t1 <- getCPUTimeDouble

  readIORef arrsRef >>= (print . sum)

  print (t1 - t0)

Интересно, что когда я тестирую это, я нахожу, что те же массивы массивов с размерами кучи влияют на производительность в большей степени, чем [Integer]. Например.

         Baseline  20M   200M
Lists:   0.7       1.0    4.4
Arrays:  0.7       2.6   20.4

Выводы (WIP)

  • Это, скорее всего, связано с поведением GC

  • Но изменчивые распакованные массивы, похоже, приводят к более сильному замедлению (см. выше). Установка +RTS -A200M обеспечивает производительность версии мусора массива в соответствии с версией списка, поддерживая, что это связано с GC.

  • Замедление пропорционально количеству выделенных массивов, а не количеству полных ячеек в массиве. Вот набор прогонов, показывающий для аналогичного теста main4 влияние количества массивов, выделенных как на время, затраченное на выделение, так и полностью несвязанную "полезную нагрузку". Это для 16777216 полных ячеек (разделенных между многими массивами):

       Array size   Array create time  Time for "payload":
       8            3.164           14.264
       16           1.532           9.008
       32           1.208           6.668
       64           0.644           3.78
       128          0.528           2.052
       256          0.444           3.08
       512          0.336           4.648
       1024         0.356           0.652
    

    И запуск этого же теста в ячейках 16777216*4 показывает в основном одинаковое время полезной нагрузки, как указано выше, только сдвинутое вниз на два места.

  • Из того, что я понимаю о том, как работает GHC, и смотря на (3), я думаю, что это накладные расходы могут быть просто из-за указаний на все эти массивы, торчащие в сохраненный набор (см. также: здесь) и любые служебные данные, которые приводят к GC.

4b9b3361

Ответ 1

Вы платите линейные накладные расходы за каждый младший GC за изменчивый массив, который остается в живых и получает повышение до старого поколения. Это связано с тем, что GHC безоговорочно помещает все изменяемые массивы в изменяемый список и перемещает весь список на каждый младший GC. Подробнее см. https://ghc.haskell.org/trac/ghc/ticket/7662, а также ответ моей рассылки на ваш вопрос: http://www.haskell.org/pipermail/glasgow-haskell-users/2014-May/024976.html

Ответ 2

Я думаю, что вы определенно видите GC-эффекты. У меня была связанная проблема в маниоке (https://github.com/tibbe/cassava/issues/49#issuecomment-34929984), где время GC увеличивалось линейно с увеличением размера кучи.

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

Вы можете уменьшить время GC при игре с опциями +RTS. Например, попробуйте установить -A в свой размер кеша L3.