Изменить: Оказывается, что вещи вообще (а не только операции массива /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,()))
]
Мы создаем новый массив и добавляем его в стек. Поскольку критерий делает больше выборок и стек растет, создание массива занимает больше времени, и это, кажется, растет линейно и регулярно:
Более странно, читаются и записываются 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,()))
]
Любая из двух строк, которые прокомментированы, избавляется от этого поведения, но я не уверен, почему (возможно, после того, как мы заставили позвоночник списка, элементы могут фактически собраться).
Вопросы
- Что здесь происходит?
- Ожидается ли поведение?
- Есть ли способ избежать этого замедления?
Изменить. Я предполагаю, что это связано с тем, что 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.