Tulisan ini menjelaskan secara rinci cara meningkatkan Calling Convention pada bahasa Rust.
Masalah pada Calling Convention Rust saat ini
- Saat ini Rust tidak memiliki calling convention yang didefinisikan secara jelas
- Dalam praktiknya, Rust memakai calling convention C bawaan dari LLVM
- Saat ini Rust secara konservatif berusaha menghasilkan signature fungsi LLVM seperti yang kemungkinan akan dihasilkan Clang
- demi kompatibilitas dengan debugger
- untuk menghindari bug LLVM
- Namun pendekatan ini terlalu konservatif, sehingga menghasilkan kode yang buruk bahkan untuk fungsi sederhana
fn extract(arr: [i32; 3]) -> i32 { arr[1] }
- Kode di atas seharusnya diteruskan lewat register, tetapi malah diteruskan lewat pointer
- Rust bahkan lebih konservatif daripada ABI C. Jika ditandai dengan
extern "C", nilainya akan diteruskan lewat register.
Usulan calling convention baru
- Untuk fungsi
extern "Rust", calling convention yang lama tetap dipertahankan - Tambahkan flag
-Zcallconvuntuk mengatur calling convention fungsiextern "Rust"-Zcallconv=legacyadalah cara saat ini-Zcallconv=fastadalah cara baru yang akan dirancang
- Mengapa calling convention lama harus dipertahankan?
- demi kemudahan debugging, tata letaknya tidak mengikuti urutan ABI C
- beberapa target seperti WASM mungkin tidak mendukungnya
- pada build debug, hal ini bisa jadi tidak ada gunanya
- Hal-hal yang perlu diperhatikan terkait function pointer dan blok
extern "Rust" {}- Karena ini adalah flag per-crate, function pointer tidak bisa menggunakannya
- Pemanggilan lewat function pointer lambat dan jarang, jadi memakai
-Zcallconv=legacy - Jika perlu, buat shim untuk mengonversi calling convention
- Untuk pemanggilan langsung seperti
extern "Rust" { fn my_func() -> i32; }- hanya simbol yang tidak di-mangle yang bisa dipanggil
- fungsi
#[no_mangle]memakai calling convention lama
Cara memanfaatkan LLVM
- Idealnya kita bisa menentukan calling convention langsung di LLVM, tetapi secara realistis itu sulit
- Bisa diakali dengan prosedur berikut
- untuk target tertentu, tentukan jumlah maksimum nilai yang bisa diteruskan lewat register
- tentukan bagaimana return value akan diteruskan. Jika muat di register, kembalikan langsung; jika besar, teruskan lewat referensi
- dari argumen yang diteruskan sebagai nilai, pilih mana yang harus diteruskan sebagai referensi
- yang lebih besar daripada ruang yang bisa diteruskan lewat register
- pada x86, sekitar 176 byte
- tentukan argumen mana yang diteruskan lewat register agar pemakaian ruang register semaksimal mungkin
- ini adalah masalah NP-hard sehingga butuh heuristik
- sisanya diteruskan lewat stack
- buat signature fungsi dalam LLVM IR
- argumen yang diteruskan lewat register direpresentasikan sebagai non-aggregate seperti i64, ptr, double,
<2 x i64>dan sejenisnya - argumen yang diteruskan lewat stack mengikuti "register input"
- argumen yang diteruskan lewat register direpresentasikan sebagai non-aggregate seperti i64, ptr, double,
- buat function prologue
- decode argumen level Rust dari register input sehingga menghasilkan nilai
%ssayang sama seperti pada-Zcallconv=legacy - body fungsi bisa menghasilkan kode yang sama terlepas dari calling convention
- kode decode yang tidak perlu akan dihapus oleh DCE
- decode argumen level Rust dari register input sehingga menghasilkan nilai
- buat blok return fungsi
- berisi instruksi phi untuk tipe return yang sama seperti pada
-Zcallconv=legacy - encode ke format output yang diperlukan lalu kembalikan dengan
ret - alih-alih
ret, alur harus bercabang ke blok ini
- berisi instruksi phi untuk tipe return yang sama seperti pada
- jika ada fungsi non-polimorfik dan non-inline yang bisa dipakai sebagai function pointer
- ketika diekspos ke luar crate atau diteruskan sebagai function pointer
- buat shim yang memakai
-Zcallconv=legacylalu melakukan tail call ke implementasi sebenarnya - ini diperlukan untuk mempertahankan kesetaraan function pointer
Cara memeriksa batas penerusan register di LLVM
- Ada program LLVM untuk memeriksa jumlah maksimum penerusan lewat register yang diizinkan LLVM
- Pada x86, input bisa berupa 6 integer dan 8 vektor SSE, sedangkan output bisa berupa 3 integer dan 4 vektor SSE
- Pada aarch64, input dan output sama-sama 8 integer dan 8 vektor
- Jika melampaui batas ini, nilai akan diteruskan lewat stack
Penanganan struct dan enum di Rust
- Diasumsikan rustc sudah mengubahnya menjadi aggregate dan union dasar
- Penanganan return value
- yang penting bukan ukuran struct, melainkan ukuran data sebenarnya tanpa padding
[(u64, u32); 2]berukuran 32 byte, tetapi jika padding 8 byte dikeluarkan maka data nyatanya 24 byte- definisikan ukuran efektif (Effective Size) suatu tipe
- yaitu jumlah bit terdefinisi, tidak termasuk padding
[(u64, u32); 2]adalah 192 bitbooladalah 1 bit
- jika ukuran efektif lebih kecil daripada ruang register output, kembalikan sebagai nilai
- pada x86, 3 integer + 4 SSE = 88 byte = 704 bit
- Penanganan register argumen
- ini adalah masalah Knapsack yang bersifat NP-hard
- heuristik sederhana
- jika ukuran efektif lebih besar daripada seluruh ruang register input, teruskan sebagai referensi
- enum diganti menjadi pasangan discriminator-union
- union bisa menyentuh bit yang tidak terinisialisasi, jadi diteruskan sebagai array
u8atau satu varian tak-kosong - ratakan menjadi elemen paling dasar seperti pointer, integer, float, boolean, dan sejenisnya
- urutkan naik berdasarkan ukuran efektif
- alokasikan prefiks terbesar yang masih muat ke register, sisanya ke stack
- jika sebagian input yang menuju stack lebih besar daripada kelipatan kecil dari ukuran pointer, teruskan lewat pointer ke stack
- selebihnya diteruskan langsung ke stack dalam urutan sebelum pengurutan
- yang diteruskan lewat register dialokasikan dalam urutan ukuran menurun
- boolean di-bit-pack per 64 buah
Pendapat GN+
- Secara pribadi, calling convention Rust saat ini sangat disayangkan. Seharusnya Rust bisa menghasilkan performa yang jauh lebih baik daripada C++, tetapi itu belum terjadi
- Ini adalah pendekatan yang sudah lama diimplementasikan oleh Go
- Alasan Rust belum bisa menerapkannya
- pembuatan kode ABI rumit dan LLVM tidak banyak membantu
- tidak banyak orang di tim compiler yang benar-benar paham LLVM
- ada kekhawatiran soal waktu kompilasi, tetapi karena hanya akan dipakai pada build optimisasi, mestinya bukan masalah besar
- Penulis tidak punya waktu untuk memperbaikinya sendiri, tetapi bersedia membantu tim compiler Rust dengan keahliannya di LLVM
- Atau, sekalian saja beralih ke
extern "C"atauextern "fastcall"juga bisa menjadi alternatif
1 komentar
Opini Hacker News
Ringkasan:
Option<u8>berukuran 16 byte di Rust, sedangkan di C 9 byte.&Option<T>atau&mut Option<T>.repr.