Editar: Il s'avère que les choses en général (pas seulement les opérations sur les tableaux/réf) ralentissent au fur et à mesure que des tableaux sont créés, donc je suppose que cela pourrait simplement mesurer l'augmentation des temps GC et ne pas être aussi étrange que je le pensais. Mais j'aimerais vraiment savoir (et apprendre comment le découvrir) ce qui se passe ici, et s'il y a un moyen d'atténuer cet effet dans un code qui crée beaucoup de petits tableaux. La question originale suit.
En examinant des résultats d'analyse comparative bizarres dans une bibliothèque, je suis tombé sur un comportement que je ne comprends pas, bien qu'il puisse être vraiment évident. Il semble que le temps pris pour de nombreuses opérations (création d'un nouveau fichier MutableArray
la lecture ou la modification d'un IORef
) augmente proportionnellement au nombre de tableaux en mémoire.
Voici le premier exemple :
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,()))
]
Nous créons un nouveau tableau et l'ajoutons à une pile. Au fur et à mesure que criterion fait plus d'échantillons et que la pile grandit, la création du tableau prend plus de temps, et cela semble croître linéairement et régulièrement :
Encore plus étrange, IORef
les lectures et les écritures sont affectées, et nous pouvons voir que les atomicModifyIORef'
de plus en plus rapide, probablement parce que plus de tableaux sont 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,()))
]
L'une ou l'autre des deux lignes commentées permet de se débarrasser de ce comportement mais je ne sais pas trop pourquoi (peut-être qu'après avoir forcé la colonne de la liste, les éléments peuvent effectivement être collectés).
Questions
- Qu'est-ce qui se passe ici ?
- Est-ce un comportement attendu ?
- Existe-t-il un moyen d'éviter ce ralentissement ?
Modifier : Je suppose que cela a quelque chose à voir avec le fait que la GC prend plus de temps, mais j'aimerais comprendre plus précisément ce qui se passe, notamment dans le premier benchmark.
Exemple de bonus
Enfin, voici un programme de test simple qui peut être utilisé pour pré-allouer un certain nombre de tableaux et chronométrer un certain nombre d'événements. atomicModifyIORef
s. Cela semble présenter le comportement lent de 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)
Un profil de tas avec -hy
montre surtout MUT_ARR_PTRS_CLEAN
ce que je ne comprends pas tout à fait.
Si vous voulez reproduire, voici le fichier cabal que j'ai utilisé
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
Modifier : Voici un autre programme de test, qui peut être utilisé pour comparer le ralentissement avec des tas de tableaux de même taille par rapport à des tas de tableaux de même taille. [Integer]
. Il faut faire des essais et des erreurs pour s'adapter n
et en observant le profilage pour obtenir des résultats comparables.
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)
Il est intéressant de noter que lorsque je teste cette méthode, je constate que la même taille de tas de tableaux affecte davantage les performances que [Integer]
. Par exemple
Baseline 20M 200M
Lists: 0.7 1.0 4.4
Arrays: 0.7 2.6 20.4
Conclusions (WIP)
-
Ceci est très probablement dû au comportement du GC
-
Mais les tableaux mutables sans boîte semblent conduire à des ralentissements plus sévères (voir ci-dessus). Réglage de
+RTS -A200M
ramène les performances de la version garbage array en ligne avec la version list, en soutenant que cela a à voir avec le GC. -
Le ralentissement est proportionnel au nombre de tableaux alloués, et non au nombre total de cellules dans le tableau. Voici un ensemble d'exécutions montrant, pour un test similaire à celui de
main4
Les effets du nombre de tableaux alloués à la fois sur le temps d'allocation et sur une "charge utile" totalement indépendante. Ceci pour un total de 16777216 cellules (réparties sur un nombre illimité de tableaux) :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
Et en exécutant ce même test sur
16777216*4
montre des temps de chargement pratiquement identiques à ceux indiqués ci-dessus, mais décalés de deux places vers le bas. -
D'après ce que j'ai compris du fonctionnement de GHC, et en regardant (3), je pense que ce surcoût pourrait être simplement dû au fait que des pointeurs vers tous ces tableaux restent dans le répertoire de l'utilisateur. jeu mémorisé (voir aussi : ici ), et tout ce que cela entraîne comme surcharge pour le GC.