Qu'est-ce qu'un «gros pointeur» dans Rust?

91

J'ai déjà lu le terme "gros pointeur" dans plusieurs contextes, mais je ne suis pas sûr de ce qu'il signifie exactement et quand il est utilisé dans Rust. Le pointeur semble être deux fois plus grand qu'un pointeur normal, mais je ne comprends pas pourquoi. Cela semble également avoir quelque chose à voir avec les objets de trait.

Lukas Kalbertodt
la source
7
Le terme lui-même n'est pas spécifique à Rust, BTW. Le pointeur gras fait généralement référence à un pointeur qui stocke des données supplémentaires en plus de l'adresse de l'objet pointé. Si le pointeur contient des bits d'étiquette et en fonction de ces bits d'étiquette, le pointeur n'est parfois pas du tout un pointeur, il est appelé une représentation de pointeur étiqueté . (Par exemple, sur de nombreuses machines virtuelles Smalltalks, les pointeurs qui se terminent par 1 bit sont en fait des entiers de 31/63 bits, car les pointeurs sont alignés sur les mots et ne se terminent donc jamais par 1.) La JVM HotSpot appelle ses gros pointeurs POO (orientés objet Pointeurs).
Jörg W Mittag
1
Juste une suggestion: lorsque je poste une paire de questions-réponses, j'écris normalement une petite note expliquant qu'il s'agit d'une question à réponse spontanée et pourquoi j'ai décidé de la publier. Jetez un œil à la note de bas de page dans la question ici: stackoverflow.com/q/46147231/5768908
Gerardo Furtado
@GerardoFurtado J'ai initialement publié un commentaire ici expliquant exactement cela. Mais il a été supprimé maintenant (pas par moi). Mais oui, je suis d'accord, souvent une telle note est utile!
Lukas Kalbertodt
@ Jörg W Mittag 'pointeurs d'objets ordinaires', en fait
obataku

Réponses:

102

Le terme "gros pointeur" est utilisé pour désigner des références et des pointeurs bruts vers des types dimensionnés dynamiquement (DST) - des tranches ou des objets de trait. Un gros pointeur contient un pointeur plus quelques informations qui rendent le DST "complet" (par exemple la longueur).

Les types les plus couramment utilisés dans Rust ne sont pas des DST, mais ont une taille fixe connue au moment de la compilation. Ces types implémentent le Sizedtrait . Même les types qui gèrent un tampon de tas de taille dynamique (comme Vec<T>) le sont, Sizedcar le compilateur connaît le nombre exact d'octets qu'une Vec<T>instance prendra sur la pile. Il existe actuellement quatre types différents de DST dans Rust.


Tranches ( [T]et str)

Le type [T](pour tout T) est dimensionné dynamiquement (de même que le type spécial "string slice" str). C'est pourquoi vous ne le voyez généralement que comme &[T]ou &mut [T], c'est-à-dire derrière une référence. Cette référence est ce que l'on appelle un "gros pointeur". Allons vérifier:

dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());

Cela imprime (avec quelques nettoyages):

size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>()    = 16

Nous voyons donc qu'une référence à un type normal comme u32est de 8 octets de large, tout comme une référence à un tableau [u32; 2]. Ces deux types ne sont pas des DST. Mais comme [u32]c'est un DST, la référence à celui-ci est deux fois plus grande. Dans le cas des tranches, les données supplémentaires qui "complètent" le DST sont simplement la longueur. On pourrait donc dire que la représentation de &[u32]est quelque chose comme ceci:

struct SliceRef { 
    ptr: *const u32, 
    len: usize,
}

Objets de trait ( dyn Trait)

Lorsque vous utilisez des traits comme objets de trait (c'est-à-dire que le type est effacé, distribué dynamiquement), ces objets de trait sont des DST. Exemple:

trait Animal {
    fn speak(&self);
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) {
        println!("meow");
    }
}

dbg!(size_of::<&Cat>());
dbg!(size_of::<&dyn Animal>());

Cela imprime (avec quelques nettoyages):

size_of::<&Cat>()        = 8
size_of::<&dyn Animal>() = 16

Encore une fois, il &Catne fait que 8 octets car il Cats'agit d'un type normal. Mais dyn Animalc'est un objet trait et donc dimensionné dynamiquement. En tant que tel, &dyn Animalest de 16 octets.

Dans le cas des objets de trait, les données supplémentaires qui complètent le DST sont un pointeur vers la vtable (le vptr). Je ne peux pas expliquer complètement le concept de vtables et de vptrs ici, mais ils sont utilisés pour appeler l'implémentation de méthode correcte dans ce contexte de répartition virtuelle. La vtable est une donnée statique qui ne contient essentiellement qu'un pointeur de fonction pour chaque méthode. Avec cela, une référence à un objet trait est essentiellement représentée par:

struct TraitObjectRef {
    data_ptr: *const (),
    vptr: *const (),
}

(Ceci est différent du C ++, où le vptr pour les classes abstraites est stocké dans l'objet. Les deux approches présentent des avantages et des inconvénients.)


DST personnalisés

Il est en fait possible de créer vos propres DST en ayant une structure où le dernier champ est un DST. C'est plutôt rare, cependant. Un exemple frappant est std::path::Path.

Une référence ou un pointeur vers le DST personnalisé est également un gros pointeur. Les données supplémentaires dépendent du type de DST à l'intérieur de la structure.


Exception: types externes

Dans la RFC 1861 , la extern typefonctionnalité a été introduite. Les types externes sont également des DST, mais les pointeurs vers eux ne sont pas des pointeurs gras. Ou plus exactement, comme le dit la RFC:

Dans Rust, les pointeurs vers les DST transportent des métadonnées sur l'objet pointé. Pour les chaînes et les tranches, il s'agit de la longueur du tampon, pour les objets de trait, c'est la vtable de l'objet. Pour les types externes, les métadonnées sont simples (). Cela signifie qu'un pointeur vers un type extern a la même taille qu'un usize(c'est-à-dire qu'il ne s'agit pas d'un "gros pointeur").

Mais si vous n'interagissez pas avec une interface C, vous n'aurez probablement jamais à gérer ces types externes.




Ci-dessus, nous avons vu les tailles des références immuables. Les pointeurs Fat fonctionnent de la même manière pour les références mutables, les pointeurs bruts immuables et les pointeurs bruts mutables:

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16
Lukas Kalbertodt
la source