Код становится медленнее по мере того, как распределяются больше массивов в коробке
Изменить: Оказывается, что вещи вообще (а не только операции массива /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.
Ответы
Ответ 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.