O que é um “ponteiro gordo”?

95

Já li o termo "apontador gordo" em vários contextos, mas não tenho certeza do que significa exatamente e quando é usado no Rust. O ponteiro parece ter o dobro do tamanho de um ponteiro normal, mas não entendo por quê. Também parece ter algo a ver com objetos de características.

Lukas Kalbertodt
fonte
7
O próprio termo não é específico da ferrugem, BTW. Ponteiro gordo geralmente se refere a um ponteiro que armazena alguns dados extras além do endereço do objeto que está sendo apontado. Se o ponteiro contém alguns bits de tag e dependendo desses bits de tag, o ponteiro às vezes não é um ponteiro, é chamado de representação de ponteiro com tag . (Por exemplo, em muitas VMs Smalltalks, os ponteiros que terminam com 1 bit são, na verdade, inteiros de 31/63 bits, uma vez que os ponteiros são alinhados por palavra e, portanto, nunca terminam em 1.) O HotSpot JVM chama seus ponteiros gordos OOP s (Orientados a Objetos Ponteiros).
Jörg W Mittag
2
Só uma sugestão: quando posto um par de perguntas e respostas, normalmente escrevo uma pequena nota explicando que é uma pergunta respondida por mim mesmo e por que decidi postá-la. Dê uma olhada na nota de rodapé na pergunta aqui: stackoverflow.com/q/46147231/5768908
Gerardo Furtado
@GerardoFurtado Inicialmente postei um comentário aqui explicando exatamente isso. Mas foi removido agora (não por mim). Mas sim, concordo, muitas vezes essa nota é útil!
Lukas Kalbertodt

Respostas:

110

O termo "ponteiro gordo" é usado para se referir a referências e ponteiros brutos para tipos de tamanho dinâmico (DSTs) - fatias ou objetos de traço. Um ponteiro grande contém um ponteiro mais algumas informações que tornam o DST "completo" (por exemplo, o comprimento).

Os tipos mais comumente usados ​​no Rust não são DSTs, mas têm um tamanho fixo conhecido em tempo de compilação. Esses tipos implementam o Sizedtraço . Mesmo os tipos que gerenciam um buffer de heap de tamanho dinâmico (como Vec<T>) são Sizedporque o compilador sabe o número exato de bytes que uma Vec<T>instância ocupará na pilha. Atualmente, existem quatro tipos diferentes de DSTs em Rust.


Fatias ( [T]e str)

O tipo [T](para qualquer um T) é dimensionado dinamicamente (assim como o tipo especial de "segmento de string" str). É por isso que normalmente você só o vê como &[T]ou &mut [T], ou seja, atrás de uma referência. Esta referência é um denominado "apontador gordo". Vamos checar:

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

Isso imprime (com alguma limpeza):

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

Portanto, vemos que uma referência a um tipo normal como u32tem 8 bytes de tamanho, pois é uma referência a um array [u32; 2]. Esses dois tipos não são DSTs. Mas, como [u32]é um DST, a referência a ele é duas vezes maior. No caso de fatias, os dados adicionais que "completam" o DST são simplesmente o comprimento. Então, pode-se dizer que a representação de &[u32]é algo assim:

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

Objetos de traço ( dyn Trait)

Ao usar características como objetos de características (ou seja, tipo apagado, despachado dinamicamente), esses objetos de características são DSTs. Exemplo:

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>());

Isso imprime (com alguma limpeza):

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

Novamente, &Cattem apenas 8 bytes porque Caté um tipo normal. Mas dyn Animalé um objeto de característica e, portanto, dimensionado dinamicamente. Como tal, &dyn Animaltem 16 bytes de tamanho.

No caso de objetos de traço, os dados adicionais que completam o DST são um ponteiro para a vtable (vptr). Não posso explicar completamente o conceito de vtables e vptrs aqui, mas eles são usados ​​para chamar a implementação do método correto neste contexto de envio virtual. O vtable é um dado estático que basicamente contém apenas um ponteiro de função para cada método. Com isso, uma referência a um objeto de traço é basicamente representada como:

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

(Isso é diferente de C ++, onde o vptr para classes abstratas é armazenado dentro do objeto. Ambas as abordagens têm vantagens e desvantagens.)


DSTs personalizados

Na verdade, é possível criar seus próprios DSTs por meio de uma estrutura em que o último campo é um DST. Porém, isso é bastante raro. Um exemplo importante é std::path::Path.

Uma referência ou ponteiro para o horário de verão personalizado também é um ponteiro grande. Os dados adicionais dependem do tipo de DST dentro da estrutura.


Exceção: tipos externos

No RFC 1861 , o extern typerecurso foi introduzido. Tipos externos também são DSTs, mas os ponteiros para eles não são ponteiros gordos. Ou mais exatamente, como diz o RFC:

No Rust, os ponteiros para DSTs carregam metadados sobre o objeto que está sendo apontado. Para strings e fatias, esse é o comprimento do buffer, para objetos de característica, essa é a vtable do objeto. Para tipos externos, os metadados são simples (). Isso significa que um ponteiro para um tipo externo tem o mesmo tamanho que um usize(ou seja, não é um "ponteiro gordo").

Mas se você não estiver interagindo com uma interface C, provavelmente nunca terá que lidar com esses tipos externos.




Acima, vimos os tamanhos para referências imutáveis. Ponteiros de gordura funcionam da mesma forma para referências mutáveis, ponteiros brutos imutáveis ​​e ponteiros brutos mutáveis:

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