32 votes

Combien de mémoire utilise un thunk?

Disons que j'ai un très grand nombre (millions/milliards+) de ces simples Foo des structures de données:

data Foo = Foo
    { a :: {-# UNPACK #-}!Int
    , b :: Int
    }

Avec donc beaucoup de ces flottant autour, il devient nécessaire de penser à la quantité de mémoire qu'ils consomment.

Sur une machine 64 bits, chaque Int est de 8 octets, de sorte a seulement 8 octets (parce que c'est la stricte et décompressé). Mais la quantité de mémoire b prendre? J'imagine que ce serait changer selon que le thunk est évalué ou pas, non?

J'imagine que dans le cas général, c'est impossible à dire, car b pourrait dépendre d'un certain nombre de positions de mémoire qui ne sont que de rester dans la mémoire en cas b doit être évaluée. Mais que faire si b ne dépendait que (certains très coûteux sur) a? Alors, est-il déterministe façon de dire quelle est la quantité de mémoire utilisée?

31voto

Joachim Breitner Points 9238

En plus de user239558 réponse, et en réponse à votre commentaire, je tiens à souligner certains des outils qui vous permettent d'inspecter le segment de la représentation de votre valeur, de trouver des réponses à des questions de ce genre vous-même et de voir l'effet des optimisations et des différentes façons de compilation.

ghc-données. datasize

vous indique la taille d'une fermeture. Ici vous pouvez voir que (sur un ordinateur 64 bits) dans évalué forme et après la collecte des ordures, Foo 1 2 nécessite 24 octets sur son propre, y compris les dépendances, 40 octets au total:

Prélude de GHC.Données. datasize Test> let x = Foo 1 2
Prélude de GHC.Données. datasize Test> x
Foo {a = 1, b = 2}
Prélude de GHC.Données. Datasize Test> Système.Mem.performGC
Prélude de GHC.Données. datasize Test> closureSize x
24
Prélude de GHC.Données. datasize Test> recursiveSize x
40

Pour reproduire ce que vous devez charger les données de définition en forme compilée avec -O, sinon, l' {-# UNPACK #-} pragma n'a aucun effet.

Maintenant, laissez-nous créer un thunk et de voir que la taille va considérablement:

Prélude de GHC.Données. datasize Test> laissez thunk = 2 + 3::Int
Prélude de GHC.Données. datasize Test> let x = Foo 1 thunk
Prélude de GHC.Données. datasize Test> x `seq` return ()
Prélude de GHC.Données. Datasize Test> Système.Mem.performGC
Prélude de GHC.Données. datasize Test> closureSize x
24
Prélude de GHC.Données. datasize Test> recursiveSize x
400

Maintenant, c'est tout à fait excessive. La raison en est que ce calcul inclut des références à la statique des fermetures, Num typeclass dictionnaires et autres, et en général la GHCi bytecode est très unoptimized. Donc, nous allons mettre cela dans une bonne Haskell programme. L'exécution de

main = do
    l <- getArgs
    let n = length l
    n `seq` return ()
    let thunk = trace "I am evaluated" $ n + n
    let x = Foo 1 thunk
    a x `seq` return ()
    performGC
    s1 <- closureSize x
    s2 <- closureSize thunk
    r <- recursiveSize x
    print (s1, s2, r)

donne (24, 24, 48), alors maintenant, l' Foo de la valeur est constituée d' Foo lui-même et un thunk. Pourquoi seulement le thunk, ne devrait-il pas être aussi une référence à l' n l'ajout d'un autre de 16 octets? Pour répondre à cela, nous avons besoin d'un meilleur outil:

ghc-tas-vue

Cette bibliothèque (par moi) peut enquêter sur le tas et vous dire précisément comment vos données y est représenté. Ainsi, l'ajout de cette ligne dans le fichier ci-dessus:

buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree

nous obtenons (quand on passe par les cinq paramètres du programme) le résultat Foo (_thunk 5) 1. Notez que l'ordre des arguments est échangé sur le tas, parce que les pointeurs viennent toujours avant les données. La plaine 5 indique que la fermeture de la thunk stocke ses argument "unboxed".

En dernier exercice, nous avons le vérifier en faisant le thunk paresseux en n: Maintenant

main = do
    l <- getArgs
    let n = length l
    n `seq` return ()
    let thunk = trace "I am evaluated" $ n
    let x = Foo 1 thunk
    a x `seq` return ()
    performGC
    s1 <- closureSize x
    s2 <- closureSize thunk
    s3 <- closureSize n
    r <- recursiveSize x
    buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree
    print (s1, s2, s3, r)

donne un tas de représentation de l' Foo (_thunk (I# 4)) 1 avec une part de fermeture pour n (comme indiqué par la présence de l' I# constructeur) et montre les tailles attendues pour les valeurs et leur total, (24,24,16,64).

Oh, et si cela est encore trop haut niveau, getClosureRaw vous donne les premières octets.

13voto

user239558 Points 1548

Si b est évalué, il sera un pointeur vers un Int objet. Le pointeur est de 8 octets, et l' Int objet se compose d'un en-tête qui est de 8 octets, et l' Int# , qui est de 8 octets.

Donc, dans ce cas, l'utilisation de la mémoire est l' Foo objet (8 en-tête, 8 Int, 8 pointeur) + coffret Int (8 en-tête, 8 Int#).

Lors de l' b est non évaluée, l'octet de 8 pointeur en Foo sera point à un Thunk objet. Le Thunk objet représente un non évaluée expression. Comme l' Int objet, cet objet a une de 8 octets d'en-tête, mais le reste de l'objet comprend les variables libres dans le non évaluée expression.

Alors tout d'abord, le nombre de variables libres organisées dans cette thunk objet dépend de l'expression qui crée l'objet Foo. Différentes façons de créer un Foo ont thunk objets potentiellement différentes tailles créé.

Puis d'autre part, les variables libres sont toutes les variables qui sont mentionnés dans la non évaluée expression que sont prises à partir de l'extérieur de l'expression, appelé environnement d'une fermeture. Ils sont en quelque sorte des paramètres de l'expression et ils doivent être stockés quelque part, et donc ils sont stockés dans le thunk objet.

De sorte que vous pouvez regarder sur les lieux mêmes où les Foo constructeur est appelé et regardez le nombre de variables libres dans le deuxième paramètre à estimer la taille de la thunk.

Un Thunk objet est vraiment le même que la fermeture de la plupart des autres langages de programmation, avec une différence importante. Lorsqu'il est évalué, il peut être remplacé par une redirection pointeur vers l'objet évalué. C'est donc une fermeture automatiquement memoizes son résultat.

Cette redirection pointeur pointera à l' Int objet (16 octets). Cependant, le maintenant "morte" thunk seront éliminés lors de la prochaine collecte de déchets. Lorsque le GC copies Foo, il fera Foo b pointer directement vers l'Int objet, ce qui le thunk non référencées et donc les ordures.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X