web-dev-qa-db-fra.com

Pourquoi un int dans OCaml ne comporte que 31 bits?

Je n'ai vu cette "fonctionnalité" nulle part ailleurs. Je sais que le 32e bit est utilisé pour la collecte des ordures. Mais pourquoi en est-il ainsi uniquement pour les pouces et non pour les autres types de base?

113
Daniel Velkov

Ceci est appelé une représentation pointeur étiqueté, et est une astuce d'optimisation assez courante utilisée dans de nombreux interpréteurs, machines virtuelles et systèmes d'exécution différents pendant des décennies. Presque toutes les implémentations LISP les utilisent, de nombreuses machines virtuelles Smalltalk, de nombreux interprètes Ruby, etc.

Habituellement, dans ces langues, vous passez toujours des pointeurs vers des objets. Un objet lui-même se compose d'un en-tête d'objet, qui contient des métadonnées d'objet (comme le type d'un objet, sa ou ses classes, peut-être des restrictions de contrôle d'accès ou des annotations de sécurité, etc.), puis les données réelles de l'objet lui-même. Ainsi, un simple entier serait représenté comme un pointeur plus un objet composé de métadonnées et de l'entier réel. Même avec une représentation très compacte, c'est quelque chose comme 6 octets pour un entier simple.

En outre, vous ne pouvez pas transmettre un tel objet entier à la CPU pour effectuer une arithmétique rapide des entiers. Si vous voulez ajouter deux entiers, vous vraiment n'avez que deux pointeurs, qui pointent vers le début des en-têtes d'objet des deux objets entiers que vous souhaitez ajouter. Ainsi, vous devez d'abord effectuer une arithmétique entière sur le premier pointeur pour y ajouter le décalage dans l'objet où les données entières sont stockées. Ensuite, vous devez déréférencer cette adresse. Faites de même avec le deuxième entier. Vous avez maintenant deux nombres entiers que vous pouvez demander au CPU d'ajouter. Bien sûr, vous devez maintenant construire un nouvel objet entier pour contenir le résultat.

Donc, pour effectuer n addition entier, vous devez réellement effectuer trois additions entier plus deux déréférences de pointeur plus une construction d'objet. Et vous prenez près de 20 octets.

Cependant, l'astuce est qu'avec les soi-disant types de valeurs immuables comme les entiers, vous n'avez généralement pas besoin toutes les métadonnées dans l'en-tête de l'objet: vous pouvez simplement laisser tout cela étoffer, et simplement le synthétiser (ce qui est VM-nerd-parler pour "fake it"), quand quelqu'un veut regarder. Un entier toujours aura la classe Integer, il n'est pas nécessaire de stocker séparément ces informations. Si quelqu'un utilise la réflexion pour déterminer la classe d'un entier, vous répondez simplement Integer et personne ne saura jamais que vous n'avez pas réellement stocké ces informations dans l'en-tête de l'objet et qu'en fait, il n'est pas même un en-tête d'objet (ou un objet).

Ainsi, l'astuce consiste à stocker la valeur de l'objet dans le pointeur à l'objet, en regroupant efficacement les deux en un.

Il y a des processeurs qui ont en fait de l'espace supplémentaire dans un pointeur (soi-disant bits de balise) qui vous permettent de stocker des informations supplémentaires sur le pointeur dans le pointeur lui-même. Des informations supplémentaires comme "ce n'est pas réellement un pointeur, c'est un entier". Les exemples incluent le Burroughs B5000, les différentes machines LISP ou l'AS/400. Malheureusement, la plupart des processeurs traditionnels actuels ne disposent pas de cette fonctionnalité.

Cependant, il existe un moyen de sortir: la plupart des processeurs traditionnels actuels fonctionnent beaucoup plus lentement lorsque les adresses ne sont pas alignées sur les limites de Word. Certains ne prennent même pas du tout en charge l'accès non aligné.

Cela signifie qu'en pratique, tous les pointeurs seront divisibles par 4, ce qui signifie qu'ils toujours se termineront par deux bits 0. Cela nous permet de distinguer les pointeurs réels (qui se terminent par 00) Et les pointeurs qui sont en fait des entiers déguisés (ceux qui se terminent par 1). Et cela nous laisse toujours avec tous les pointeurs qui se terminent par 10 Libres de faire d'autres choses. De plus, la plupart des systèmes d'exploitation modernes se réservent les adresses très basses, ce qui nous donne une autre zone de discussion (pointeurs qui commencent, disons, 24 0 S et se terminent par 00).

Ainsi, vous pouvez encoder un entier de 31 bits en un pointeur, simplement en le décalant de 1 bit vers la gauche et en y ajoutant 1. Et vous pouvez effectuer très rapide l'arithmétique entière avec ceux-ci, en les déplaçant simplement de manière appropriée (parfois même pas nécessaire).

Que faisons-nous de ces autres espaces d'adressage? Eh bien, des exemples typiques incluent le codage floats dans l'autre grand espace d'adressage et un certain nombre d'objets spéciaux comme true, false, nil, le 127 ASCII, certaines chaînes courtes couramment utilisées, la liste vide, l'objet vide, le tableau vide et ainsi de suite près de l'adresse 0.

Par exemple, dans les interpréteurs MRI, YARV et Rubinius Ruby, les entiers sont codés comme je l'ai décrit ci-dessus, false est codé comme adresse 0 (Ce qui se produit juste aussi pour être la représentation de false en C), true comme adresse 2 (qui se trouve être la représentation C de true décalé d'un bit) et nil comme 4.

242
Jörg W Mittag

Voir la section "représentation des entiers, bits de balise, valeurs allouées en tas" de https://ocaml.org/learn/tutorials/performance_and_profiling.html pour une bonne description.

La réponse courte est que c'est pour la performance. Lors du passage d'un argument à une fonction, il est transmis sous forme d'entier ou de pointeur. Au niveau du langage machine, il n'y a aucun moyen de savoir si un registre contient un entier ou un pointeur, c'est juste une valeur de 32 ou 64 bits. Ainsi, le temps d'exécution OCaml vérifie le bit de balise pour déterminer si ce qu'il a reçu était un entier ou un pointeur. Si le bit d'étiquette est défini, la valeur est un entier et elle est transmise à la surcharge appropriée. Sinon, c'est un pointeur et le type est recherché.

Pourquoi seuls les entiers ont-ils cette balise? Parce que tout le reste est passé comme un pointeur. Ce qui est transmis est soit un entier, soit un pointeur vers un autre type de données. Avec un seul bit d'étiquette, il ne peut y avoir que deux cas.

28
shf301

Ce n'est pas exactement "utilisé pour la collecte des ordures". Il est utilisé pour distinguer en interne un pointeur et un entier non encadré.

17
Chuck

Je dois ajouter ce lien pour aider l'OP à mieux comprendre n type à virgule flottante 63 bits pour OCaml 64 bits

Bien que le titre de l'article semble à propos de float, il parle en fait de extra 1 bit

Le runtime OCaml permet le polymorphisme grâce à la représentation uniforme des types. Chaque valeur OCaml est représentée comme un seul mot, de sorte qu'il est possible d'avoir une seule implémentation pour, disons, "liste de choses", avec des fonctions pour accéder (par exemple List.length) et construire (par exemple List.map) ces listes qui fonctionnent de la même manière, qu'il s'agisse de listes d'entiers, de flottants ou de listes d'ensembles d'entiers.

Tout ce qui ne rentre pas dans un mot est alloué dans un bloc du tas. Le mot représentant ces données est alors un pointeur vers le bloc. Comme le tas ne contient que des blocs de mots, tous ces pointeurs sont alignés: leurs quelques bits de moindre poids sont toujours non définis.

Les constructeurs sans argument (comme ceci: tapez fruit = Apple | Orange | Banana) et les entiers ne représentent pas tellement d'informations qu'ils doivent être alloués dans le tas. Leur représentation n'est pas encadrée. Les données sont directement à l'intérieur de Word qui aurait autrement été un pointeur. Ainsi, alors qu'une liste de listes est en fait une liste de pointeurs, une liste d'entiers contient les entrées avec une indirection de moins. ont la même taille.

Pourtant, le garbage collector doit être capable de reconnaître les pointeurs à partir d'entiers. Un pointeur pointe vers un bloc bien formé dans le tas qui est par définition vivant (car il est visité par le GC) et doit être marqué ainsi. Un entier peut avoir n'importe quelle valeur et pourrait, si aucune précaution n'était prise, ressembler accidentellement à un pointeur. Cela pourrait faire en sorte que les blocs morts semblent vivants, mais bien pire, cela entraînerait également le GC à changer les bits dans ce qu'il pense être l'en-tête d'un bloc actif, alors qu'il suit en fait un entier qui ressemble à un pointeur et gâche l'utilisateur Les données.

C'est pourquoi les entiers sans boîte fournissent 31 bits (pour OCaml 32 bits) ou 63 bits (pour OCaml 64 bits) au programmeur OCaml. Dans la représentation, dans les coulisses, le bit le moins significatif d'un mot contenant un entier est toujours défini, pour le distinguer d'un pointeur. Les entiers 31 ou 63 bits sont plutôt inhabituels, donc quiconque utilise OCaml le sait. Ce que les utilisateurs d'OCaml ne savent généralement pas, c'est pourquoi il n'existe pas de type flottant 63 bits sans boîte pour OCaml 64 bits.

13
Jackson Tale

Pourquoi un int dans OCaml ne comporte que 31 bits?

Fondamentalement, pour obtenir les meilleures performances possibles sur le prouveur de théorèmes Coq où l'opération dominante est la mise en correspondance de modèles et les types de données dominants sont des types variantes. La meilleure représentation des données s'est avérée être une représentation uniforme utilisant des balises pour distinguer les pointeurs des données non emballées.

Mais pourquoi en est-il ainsi uniquement pour les pouces et non pour les autres types de base?

Non seulement int. D'autres types tels que char et les énumérations utilisent la même représentation balisée.

3
Jon Harrop