Baris sumber shape.area() yang sama persis bisa dikompilasi menjadi instruksi mesin yang sangat berbeda. Perbedaannya terletak pada satu pertanyaan: kapan alamat fungsi tujuan ditentukan? Ketika tipe konkrit sudah diketahui saat kompilasi, Rust menanam alamat secara langsung — inilah static dispatch, jalur yang dilalui generics melalui monomorphization. Ketika tipe konkrit disembunyikan di balik dyn Trait, alamat fungsi baru dicari saat program berjalan lewat vtable — inilah dynamic dispatch, jalur yang dilalui trait object. Chapter ini menempatkan kedua mekanisme bersebelahan, membedah langkah-langkahnya di balik layar, lalu menimbang biaya dan keuntungan masing-masing supaya kita memilih dengan sadar, bukan karena kebiasaan.
Dua Model Pengikatan Method #
Dispatch adalah proses memutuskan fungsi mana yang dijalankan ketika sebuah method dipanggil. Rust mengenal dua model. Pada static dispatch, kompilator melihat tipe konkrit pada saat kompilasi, memastikan method berasal dari impl yang mana, lalu menulis instruksi call menuju alamat fungsi tersebut — alamatnya beku di dalam binary. Pada dynamic dispatch, kompilator sengaja menunda keputusan: kode yang dihasilkan hanya berisi langkah “cari alamat fungsi di vtable, lalu panggil”, dan keputusan final baru diambil saat runtime berdasarkan tipe konkrit yang bersembunyi di balik fat pointer.
Kedua model tersebut dapat hadir dalam satu program yang sama, bahkan dalam satu trait yang sama. Kuncinya ada pada tipe yang kita pilih di tanda tangan fungsi: parameter generic &T dengan trait bound memicu static dispatch, sedangkan parameter &dyn Shape memicu dynamic dispatch.
Static Dispatch dan Monomorphization #
Mari kita susun pembandingnya secara langsung. Dua fungsi di program berikut menjalankan pekerjaan yang identik — memanggil shape.area() — dengan model dispatch yang berbeda:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rect {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.1415 * self.radius * self.radius
}
}
impl Shape for Rect {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn static_area<T: Shape>(shape: &T) -> f64 {
shape.area()
}
fn dynamic_area(shape: &dyn Shape) -> f64 {
shape.area()
}
fn main() {
let circle = Circle { radius: 7.0 };
let rect = Rect { width: 4.0, height: 5.0 };
println!("Statis -> Circle: {:.2}", static_area(&circle));
println!("Statis -> Rect : {:.2}", static_area(&rect));
println!("Dinamis -> Circle: {:.2}", dynamic_area(&circle));
println!("Dinamis -> Rect : {:.2}", dynamic_area(&rect));
}Output yang Dihasilkan:
Statis -> Circle: 153.93
Statis -> Rect : 20.00
Dinamis -> Circle: 153.93
Dinamis -> Rect : 20.00Hasil keempat pemanggilan identik — bedanya baru terasa ketika kita mengintip apa yang dilakukan kompilator terhadap baris-baris tersebut. Mari kita bedah kode di atas:
- Baris 26 (
fn static_area<T: Shape>(shape: &T) -> f64): tanda tangan generic dengan trait boundT: Shape. Setiap pemanggilan dengan tipe konkrit baru memicu monomorphization — kompilator menulis satu salinan fungsi khusus untuk tipe tersebut. - Baris 30 (
fn dynamic_area(shape: &dyn Shape) -> f64): tanda tangan trait object. Hanya ada satu salinan fungsi untuk semua tipe, dan pemilihan method ditunda sampai runtime. - Baris 38-39 (
static_area(&circle)danstatic_area(&rect)): dua pemanggilan ini memakai dua salinan berbeda daristatic_area— satu versi untukCircle, satu versi untukRect. - Baris 40-41 (
dynamic_area(&circle)dandynamic_area(&rect)): kedua pemanggilan memakai salinan yang sama. Di titik panggil,&Circledan&Rectmasing-masing di-coerce menjadi fat pointer&dyn Shapeyang membawa vtable versinya sendiri.
Monomorphization bekerja seperti kue cetak: setelah kompilator selesai, static_area tidak lagi generic di dalam binary. Secara konsep, isi binary menyerupai dua fungsi berikut:
fn static_area(shape: &Circle) -> f64 { shape.area() } // salinan untuk Circle
fn static_area(shape: &Rect) -> f64 { shape.area() } // salinan untuk RectPada setiap salinan, tipe konkrit sudah pasti, sehingga kompilator bebas menulis instruksi call langsung menuju area versi Circle atau versi Rect — bahkan dapat melangkah lebih jauh dengan menyisipkan isi fungsi langsung ke pemanggilnya (inlining) karena seluruh kode sudah terlihat sekaligus.
Dynamic Dispatch Lewat vtable #
Sisi dynamic_area menyimpan satu salinan fungsi saja, dan di dalamnya shape.area() dikompilasi menjadi rangkaian tiga langkah yang dijalankan setiap kali baris tersebut dieksekusi:
Langkah pertama membaca alamat vtable dari kata kedua fat pointer — ingat Chapter Trait Object dyn: fat pointer memuat pointer data dan pointer vtable. Langkah kedua membaca alamat fungsi area dari slot yang sesuai di dalam vtable; vtable milik pasangan Shape-Circle dan Shape-Rect memuat alamat yang berbeda, dan itulah titik keputusannya. Langkah ketiga memanggil alamat tersebut sambil mengirimkan pointer data sebagai self. Ketika dynamic_area(&circle) berjalan, rangkaian tiga langkah tersebut mendarat di area versi Circle; ketika dynamic_area(&rect) berjalan, rangkaian yang sama mendarat di area versi Rect.
Sifat “alamat baru tahu saat runtime” inilah sumber biaya sekaligus sumber kekuatan. Biayanya: dua pembacaan memori tambahan, satu instruksi indirect call yang sulit diprediksi branch predictor, dan hilangnya kesempatan inlining karena kompilator tidak melihat fungsi tujuan secara pasti. Kekuatannya: satu salinan kode untuk semua tipe, binary lebih ramping, dan tipe konkrit boleh dipilih bebas saat runtime tanpa kompilasi ulang.
Menimbang Biaya dan Keuntungan #
Dalam angka kasar, satu dynamic dispatch umumnya hanya menambah beberapa nanodetik — pada sebagian besar kode, selisih tersebut tenggelam oleh biaya I/O, alokasi, atau logika bisnis yang jauh lebih berat. Selisihnya baru terasa pada hot loop: lingkaran ketat yang dieksekusi jutaan kali per detik, tempat hilangnya inlining juga merenggut optimasi lanjutan seperti penyederhanaan aritmetika lintas pemanggilan. Arah kebalikannya juga nyata: monomorphization menggandakan kode per tipe, sehingga binary membesar dan waktu kompilasi memanjang ketika satu fungsi generic di-instantiate untuk puluhan tipe.
Ringkasan #
Static dispatch menanam alamat method saat kompilasi lewat monomorphization: cepat, bisa di-inline, tetapi menambah salinan kode per tipe. Dynamic dispatch menunda keputusan ke runtime lewat tiga langkah vtable — baca pointer vtable, baca slot method, panggil dengan pointer data — dengan biaya beberapa langkah tambahan dan satu salinan kode untuk semua tipe. Pemilihan model ditentukan oleh bentuk parameter: &T dengan trait bound untuk yang statis, &dyn Trait untuk yang dinamis. Sebelum semakin jauh memakai dyn Trait, kita perlu menguji satu batasan penting: tidak semua trait boleh dijadikan trait object. Aturannya bernama object safety, pokok bahasan Chapter berikutnya.