Empreinte mémoire des types de données Haskell

124

Comment puis-je trouver la quantité réelle de mémoire requise pour stocker une valeur d'un type de données dans Haskell (principalement avec GHC)? Est-il possible de l'évaluer à l'exécution (par exemple dans GHCi) ou est-il possible d'estimer les besoins en mémoire d'un type de données composé à partir de ses composants?

En général, si les besoins en mémoire des types aet bsont connus, quelle est la surcharge mémoire des types de données algébriques tels que:

data Uno = Uno a
data Due = Due a b

Par exemple, combien d'octets en mémoire ces valeurs occupent-elles?

1 :: Int8
1 :: Integer
2^100 :: Integer
\x -> x + 1
(1 :: Int8, 2 :: Int8)
[1] :: [Int8]
Just (1 :: Int8)
Nothing

Je comprends que l'allocation de mémoire réelle est plus élevée en raison du retard du garbage collection. Il peut être significativement différent en raison de l'évaluation paresseuse (et la taille du thunk n'est pas liée à la taille de la valeur). La question est, étant donné un type de données, combien de mémoire prend sa valeur lorsqu'elle est entièrement évaluée?

J'ai trouvé qu'il existe une :set +soption dans GHCi pour voir les statistiques de la mémoire, mais il n'est pas clair comment estimer l'empreinte mémoire d'une seule valeur.

sastanin
la source

Réponses:

156

(Ce qui suit s'applique à GHC, d'autres compilateurs peuvent utiliser des conventions de stockage différentes)

Règle de base: un constructeur coûte un mot pour un en-tête et un mot pour chaque champ . Exception: un constructeur sans champ (comme Nothingou True) ne prend pas de place, car GHC crée une seule instance de ces constructeurs et la partage entre toutes les utilisations.

Un mot est de 4 octets sur une machine 32 bits et de 8 octets sur une machine 64 bits.

Donc par exemple

data Uno = Uno a
data Due = Due a b

an Unoprend 2 mots et a en Dueprend 3.

Le Inttype est défini comme

data Int = I# Int#

maintenant, Int#prend un mot, donc Intprend 2 au total. La plupart des types sans boîte prennent un mot, les exceptions étant Int64#, Word64#et Double#(sur une machine 32 bits) qui prennent 2. GHC a en fait un cache de petites valeurs de type Intet Char, dans de nombreux cas, celles-ci ne prennent pas du tout d'espace sur le tas. A Stringnécessite uniquement de l'espace pour les cellules de la liste, sauf si vous utilisez Chars> 255.

An Int8a une représentation identique à Int. Integerest défini comme ceci:

data Integer
  = S# Int#                            -- small integers
  | J# Int# ByteArray#                 -- large integers

donc un petit Integer( S#) prend 2 mots, mais un grand entier prend une quantité d'espace variable en fonction de sa valeur. A ByteArray#prend 2 mots (en-tête + taille) plus un espace pour le tableau lui-même.

Notez qu'un constructeur défini avec newtypeest gratuit . newtypeest purement une idée au moment de la compilation, et cela ne prend pas de place et ne coûte aucune instruction au moment de l'exécution.

Plus de détails dans La disposition des objets de tas dans le commentaire du GHC .

Simon Marlow
la source
1
Merci Simon. C'est exactement ce que je voulais savoir.
sastanin le
2
L'en-tête n'est-il pas deux mots? Un pour la balise, et un pour le pointeur de transfert à utiliser pendant le GC ou l'évaluation? Cela n'ajouterait-il pas un mot à votre total?
Edward KMETT
5
@Edward: Les thunks sont écrasés par des indirections (qui sont ensuite supprimées par le GC), mais ce ne sont que 2 mots, et chaque objet de tas est garanti d'avoir une taille d'au moins 2 2 mots. Sans aucune fonctionnalité de profilage ou de débogage activée, l'en-tête n'est vraiment qu'un mot. Dans GHC, c'est-à-dire que d'autres implémentations peuvent faire les choses différemment.
nominolo
3
nominolo: oui, mais de Closure.h: / * Un thunk a un mot de remplissage pour prendre la valeur mise à jour. Ceci afin que la mise à jour n'écrase pas la charge utile, afin que nous puissions éviter d'avoir à verrouiller le thunk pendant l'entrée et la mise à jour. Remarque: cela ne s'applique pas aux THUNK_STATIC, qui n'ont pas de charge utile. Remarque: nous laissons ce mot de remplissage de toutes les manières, plutôt que simplement SMP, afin de ne pas avoir à recompiler toutes nos bibliothèques pour SMP. * / La charge utile n'est pas écrasée lors d'une indirection. L'indirection est écrite dans un emplacement séparé dans l'en-tête.
Edward KMETT
6
Oui, mais notez que ce n'est que pour les thunks . Cela ne s'applique pas aux constructeurs. Estimer la taille d'un thunk est un peu difficile de toute façon - vous devez compter les variables libres.
nominolo
4

Le package ghc-datasize fournit la fonction recursiveSize pour calculer la taille d'un objet GHC. Toutefois...

Un garbage collection est effectué avant que la taille soit calculée, car le garbage collector rendrait les parcours de tas difficiles.

... il ne serait donc pas pratique d'appeler cela souvent!

Voir également Comment trouver les représentations mémoire de GHC des types de données? et Comment puis-je déterminer la taille d'un type dans Haskell? .

mhwombat
la source