Apa yang dimaksud dengan "penunjuk lemak" di Rust?

91

Saya telah membaca istilah "penunjuk lemak" dalam beberapa konteks, tapi saya tidak yakin apa sebenarnya artinya dan kapan digunakan di Rust. Penunjuk tampaknya dua kali lebih besar dari penunjuk normal, tapi saya tidak mengerti mengapa. Ini juga tampaknya ada hubungannya dengan objek sifat.

Lukas Kalbertodt
sumber
7
Istilah itu sendiri tidak khusus untuk Karat, BTW. Penunjuk lemak umumnya mengacu pada penunjuk yang menyimpan beberapa data tambahan selain hanya alamat objek yang dituju. Jika pointer berisi beberapa bit tag dan bergantung pada bit tag tersebut, terkadang pointer bukanlah sebuah pointer, ini disebut representasi pointer yang diberi tag . (Misalnya pada banyak VM Smalltalks, pointer yang diakhiri dengan 1 bit sebenarnya adalah bilangan bulat 31/63-bit, karena pointer diselaraskan dengan kata dan karenanya tidak pernah berakhir di 1.) HotSpot JVM menyebut penunjuk lemaknya OOP (Object-Oriented) Pointer).
Jörg W Mittag
1
Hanya sebuah saran: ketika saya memposting pasangan Tanya Jawab, saya biasanya menulis catatan kecil yang menjelaskan bahwa itu adalah pertanyaan yang dijawab sendiri, dan mengapa saya memutuskan untuk mempostingnya. Lihat catatan kaki dalam pertanyaan di sini: stackoverflow.com/q/46147231/5768908
Gerardo Furtado
@GerardoFurtado Awalnya saya memposting komentar di sini menjelaskan hal itu. Tapi sekarang sudah dihapus (bukan oleh saya). Tapi ya, saya setuju, seringkali catatan seperti itu berguna!
Lukas Kalbertodt
@ Jörg W Mittag 'penunjuk objek biasa', sebenarnya
obataku

Jawaban:

102

Istilah "penunjuk lemak" digunakan untuk merujuk pada referensi dan penunjuk mentah untuk tipe berukuran dinamis (DST) - irisan atau objek sifat. Penunjuk gemuk berisi penunjuk ditambah beberapa informasi yang membuat DST "lengkap" (misalnya panjangnya).

Jenis yang paling umum digunakan di Rust bukanlah DST, tetapi memiliki ukuran tetap yang diketahui pada waktu kompilasi. Jenis menerapkan yang Sizedsifat . Bahkan jenis yang mengelola buffer heap dengan ukuran dinamis (seperti Vec<T>) adalah Sizedkarena kompilator mengetahui jumlah pasti byte yang Vec<T>akan digunakan instance di stack. Saat ini ada empat jenis DST berbeda di Rust.


Irisan ( [T]dan str)

Jenis [T](untuk apa saja T) berukuran dinamis (begitu juga jenis "potongan tali" khusus str). Itulah mengapa Anda biasanya hanya melihatnya sebagai &[T]atau &mut [T], yaitu di belakang referensi. Referensi ini disebut "penunjuk lemak". Mari kita periksa:

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

Ini mencetak (dengan beberapa pembersihan):

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

Jadi kita melihat bahwa referensi ke tipe normal seperti u32berukuran 8 byte, seperti referensi ke array [u32; 2]. Kedua tipe tersebut bukanlah DST. Tetapi seperti halnya [u32]DST, rujukannya dua kali lebih besar. Dalam kasus irisan, data tambahan yang "melengkapi" DST hanyalah panjangnya. Jadi dapat dikatakan representasi dari &[u32]sesuatu seperti ini:

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

Objek sifat ( dyn Trait)

Saat menggunakan sifat sebagai objek sifat (yaitu tipe terhapus, dikirim secara dinamis), objek sifat ini adalah DST. Contoh:

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

Ini mencetak (dengan beberapa pembersihan):

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

Sekali lagi, ukurannya &Cathanya 8 byte karena Catmerupakan tipe normal. Tetapi dyn Animalmerupakan ciri objek dan karena itu berukuran dinamis. Dengan demikian, &dyn Animalukurannya adalah 16 byte.

Dalam kasus objek sifat, data tambahan yang melengkapi DST adalah penunjuk ke vtable (vptr). Saya tidak dapat sepenuhnya menjelaskan konsep vtables dan vptrs di sini, tetapi mereka digunakan untuk memanggil implementasi metode yang benar dalam konteks pengiriman virtual ini. Vtable adalah bagian data statis yang pada dasarnya hanya berisi penunjuk fungsi untuk setiap metode. Dengan itu, referensi ke objek sifat pada dasarnya direpresentasikan sebagai:

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

(Ini berbeda dari C ++, di mana vptr untuk kelas abstrak disimpan di dalam objek. Kedua pendekatan tersebut memiliki kelebihan dan kekurangan.)


DST Kustom

Sebenarnya dimungkinkan untuk membuat DST Anda sendiri dengan memiliki struct di mana bidang terakhir adalah DST. Ini agak jarang. Salah satu contoh yang menonjol adalah std::path::Path.

Referensi atau pointer ke DST kustom juga merupakan pointer gemuk. Data tambahan tergantung pada jenis DST di dalam struct.


Pengecualian: Tipe eksternal

Di RFC 1861 , extern typefitur tersebut diperkenalkan. Jenis eksternal juga merupakan DST, tetapi petunjuk ke sana bukanlah penunjuk lemak. Atau lebih tepatnya, seperti yang dikatakan RFC:

Di Rust, pointer ke DST membawa metadata tentang objek yang diarahkan. Untuk string dan irisan ini adalah panjang buffer, untuk objek ciri ini adalah vtable objek. Untuk tipe eksternal, metadatanya sederhana (). Ini berarti bahwa penunjuk ke tipe eksternal memiliki ukuran yang sama dengan a usize(yaitu, bukan "penunjuk lemak").

Tetapi jika Anda tidak berinteraksi dengan antarmuka C, Anda mungkin tidak perlu berurusan dengan tipe eksternal ini.




Di atas, kami telah melihat ukuran untuk referensi yang tidak dapat diubah. Pointer gemuk berfungsi sama untuk referensi yang dapat diubah, pointer mentah yang tidak dapat diubah, dan pointer mentah yang dapat diubah:

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