- Konvensi pemanggilan
extern "Rust"Rust saat ini bergantung pada jalur konvensi pemanggilan C milik LLVM, dan karena pemanfaatan register untuk pengiriman nilai kompleks bersifat konservatif, peluang untuk menghasilkan kode yang lebih baik terlewatkan - Dengan flag tingkat crate
-Zcallconv, pendekatan saat ini,legacy, dipisahkan dari pendekatan baru yang berpusat pada register,fast; inti gagasannya adalah memakai ABI yang lebih agresif pada build teroptimasi - Bahkan tanpa menambahkan konvensi pemanggilan baru langsung ke LLVM, kita dapat mengendalikan penempatan argumen dengan signature fungsi LLVM yang tetap dan nilai
poison, sambil mengosongkan argumen register yang tidak dipakai tanpa biaya - Tipe Rust seperti struct, enum, union,
bool, danResultdapat dikirim lebih rapat melalui ukuran efektif yang mengecualikan padding, flattening, bit packing, serta heuristik pemisahan stack/register - Optimisasi yang lebih kuat dimungkinkan jika body fungsi, informasi borrow checker, dan informasi profil ikut tercermin dalam keputusan ABI, tetapi kompleksitas pembuatan kode ABI di rustc dan kurangnya keahlian LLVM tetap menjadi hambatan nyata
Optimisasi konvensi pemanggilan yang saat ini dilewatkan Rust
- Konvensi pemanggilan (calling convention) adalah bagian dari ABI yang menentukan bagaimana argumen fungsi dan nilai kembalian dikirim, register mana yang dipakai, serta bagaimana prolog/epilog dan unwinding ditangani
- Rust mendefinisikan konvensi pemanggilannya sendiri yang unspecified, tetapi dalam praktiknya diturunkan ke konvensi pemanggilan C bawaan LLVM dan bergantung pada pembuatan kode prolog/epilog LLVM
- rustc bertindak konservatif untuk menghasilkan signature fungsi LLVM yang kira-kira akan dibuat oleh Clang
- Ini dapat mengurangi kemungkinan debugger rusak
- Ini dapat menurunkan kemungkinan memicu bug LLVM lewat jalur pembuatan kode ABI yang jarang dipakai Clang
- Pada sistem berbasis ELF, DWARF tidak mematri ABI C Linux, sehingga dalam cakupan tulisan ini kemampuan debugging tidak dianggap sebagai masalah utama
- Sebagai contoh sederhana, pada
fn extract(arr: [i32; 3]) -> i32, array 12 byte dikirim lewat pointer, bukan register- Jika
extern "C"ditambahkan,[i32; 3]yang sama dikirim dalam bentuk packed melaluirdidanrsi - Ini adalah contoh jalur default Rust yang bahkan lebih konservatif daripada ABI C Linux
- Jika
-Zcallconv: cara memisahkan legacy dan fast
- Konvensi pemanggilan
extern "Rust"saat ini dipertahankan, tetapi konvensi pemanggilan yang dipakai dipilih dengan flag kompilasi tingkat crate-Zcallconv-Zcallconv=legacy: pendekatan saat ini-Zcallconv=fast: pendekatan baru yang dirancang dengan fokus pada register-Ojuga dapat secara otomatis menetapkan-Zcallconv=fast
- Karena konvensi pemanggilan
fasttidak menempatkan argumen sesuai urutan ABI C, orang yang mengharapkan urutan register idiomatis x86 mungkin merasa bingung - Pada target seperti WASM yang tidak memiliki konsep register dan spilling,
-Zcallconv=fastmungkin tidak didukung - Pada build debug dengan optimisasi dimatikan,
fastbisa menghasilkan kode yang lebih buruk, sehingga mungkin tidak tepat untuk diaktifkan - Pointer fungsi dan blok
extern "Rust" {}memerlukan batasan terpisah- Flag bersifat tingkat crate, tetapi sulit mengekspresikan versi
extern "Rust"mana yang dipakai oleh sebuah pointer fungsi - Pemanggilan melalui pointer fungsi dapat dianggap sebagai jalur yang lambat dan jarang, lalu dipaksa memakai
-Zcallconv=legacy - Jika perlu, shim yang mengonversi konvensi pemanggilan dibuat
- Karena ada jalur yang dapat memanggil simbol unmangled, simbol
#[no_mangle]juga dapat dibuat memakai konvensi pemanggilan legacy
- Flag bersifat tingkat crate, tetapi sulit mengekspresikan versi
Cara mengendalikan LLVM secara tidak langsung
- Idealnya, kita ingin menentukan konvensi pemanggilan langsung di LLVM, seperti “argumen ini ke register ini, nilai kembalian itu ke register itu”, tetapi menambahkan konvensi pemanggilan ke LLVM membutuhkan banyak kode C++
- Sebagai gantinya, prosedur berikut dapat memberi efek yang mendekati konvensi pemanggilan sendiri
- Menentukan jumlah maksimum nilai yang dapat dikirim lewat register untuk tiap target triple
- Menentukan apakah nilai kembalian masuk ke register output, atau harus dikembalikan by-reference sebagai argumen
ptrtambahan dengan atributsret - Menurunkan argumen by-value yang terlalu besar menjadi by-reference
- Menentukan argumen mana yang dikirim lewat register untuk memaksimalkan tingkat pemakaian ruang register
- Menaruh argumen lainnya di stack
- Signature fungsi LLVM IR disusun dari argumen non-aggregate seperti
i64,ptr,double, dan<2 x i64> - Dalam prolog fungsi, input register didekode menjadi argumen tingkat Rust
- Dalam blok akhir fungsi, nilai kembalian dienkode ke format output yang dibutuhkan, lalu
ret - Untuk fungsi non-polymorphic dan non-inline yang alamatnya dapat diambil, dibuat legacy shim agar identitas pointer fungsi tetap terjaga
- Masalah menentukan nilai mana yang dimasukkan ke register setara dengan masalah knapsack (knapsack problem) dan bersifat NP-hard, sehingga implementasi nyata membutuhkan heuristik
- Informasi ini dapat dimasukkan ke
rmetaagar tidak dihitung ulang terlalu terlambat - Rust memutus ABI di setiap rilis, sehingga syarat bahwa kode yang dibuat oleh beberapa compiler Rust berbeda tidak boleh di-link memang sudah sesuai dengan situasi saat ini
Batas pengiriman register yang diizinkan LLVM
- Saat meneruskan argumen aggregate by-value ke fungsi, LLVM berusaha “meledakkannya” ke register sebanyak mungkin
- Pada x86, input yang dapat dikirim LLVM lewat register kira-kira sebagai berikut
- 6 integer
- 8 vektor SSE
- Nilai kembalian adalah separuhnya: 3 integer dan 4 vektor
- Pada
aarch64-unknown-linux, input dan output sama-sama dapat memakai 8 integer dan 8 vektor - Semua fungsi
-Zcallconv=fastdi x86 dapat dirancang agar memiliki jumlah argumen by-register yang sama- 6 argumen untuk register integer
- 8 argumen vektor dari
xmm0hinggaxmm7 - Saat pointer sungguhan dikirim,
i64terkait diganti menjadiptr - Saat
doubledikirim, slot<2 x i64>digantikan
- Meskipun sebagian besar fungsi tidak mengirim 176 byte, biaya tambahan dapat dihindari dengan meneruskan
poisonLLVM untuk argumen yang tidak dipakai- LLVM dapat menganggap
poisonsebagai nilai yang paling nyaman saat ini - Jika
poisondikirim sebagai argumen register, ia dapat diperlakukan seperti “nilai yang sudah ada di register itu”, sehingga register tidak perlu disentuh - Dalam contoh,
load_rcx()menerima pointer melaluircx, dan kode yang memuatpoisonke 13 register lainnya tidak menghasilkan kode apa pun setelah optimisasi
- LLVM dapat menganggap
- Pendekatan ini memberi kendali hampir penuh atas pengiriman argumen, tetapi kondisi ideal saat input dan output memakai register yang sama berbeda di tiap arsitektur
- ARM dan RISC-V lebih dekat ke struktur yang memakai register yang sama untuk input dan output
- x86 tidak demikian, tetapi perpindahan register yang tidak perlu dapat dikurangi dengan mengasumsikan urutan alokasi register yang berbeda
Membuat tipe Rust lebih pas ke register
- Saat menangani struct dan union Rust, diasumsikan rustc sudah memproses tipe pengguna sebagai aggregate dan union dasar, lalu diputuskan bagian mana yang ditempatkan di register
- Pada nilai kembalian, ukuran efektif yang mengecualikan padding lebih penting daripada ukuran total struct
[(u64, u32); 2]berukuran total 32 byte, tetapi 8 byte di antaranya adalah padding- Jika di-flatten menjadi
(u64, u32, u64, u32), lalu diurutkan berdasarkan ukuran menjadi(u64, u64, u32, u32), ukurannya menjadi 24 byte - Ini dapat masuk ke 3 register kembalian integer x86
- Ukuran efektif didefinisikan sebagai jumlah bit non-
undef[(u64, u32); 2]adalah 192 bitbooladalah 1 bitcharsecara teknis 21 bit, tetapi demi penyederhanaan diperlakukan seperti aliasu32
- Struct dengan banyak
booldapat mengembalikan beberapabooldalam satu register melalui bit packing - Sisi argumen lebih sulit, dan heuristik berikut dapat diterapkan
- Argumen yang ukuran efektifnya lebih besar daripada total ruang input by-register diturunkan menjadi by-reference
- Pada x86, total ruang input adalah 176 byte, atau 1408 bit
- Enum diubah menjadi pasangan discriminant dan union
Option<i32>secara internal dapat dilihat seperti(union { i32, () }, i1)Option<Option<i32>>dapat dilihat seperti(union { i32, (), () }, i2)
- Karena union dapat menyentuh bit yang belum diinisialisasi secara sembarang, biasanya ia dikirim seperti array
u8 - Union yang hanya memiliki satu variant non-empty diganti dengan variant tersebut
- Argumen yang sudah dikonversi di-flatten menjadi primitive seperti pointer, integer, float, dan bool
- Field yang lebih besar daripada register argumen kecil, seperti
u128danf64, dapat dipecah - Daftar primitive diurutkan berdasarkan ukuran efektif, lalu prefix terbesar yang muat di register dipilih
- Sisanya diletakkan di stack
- Jika bagian yang masuk stack lebih besar daripada kelipatan kecil ukuran pointer, ia diturunkan menjadi pointer-on-the-stack untuk mengurangi traffic memori
- Nilai yang dikirim lewat register ditempatkan dari yang terbesar, dan
booldi-bit-pack hingga 64 per register
Contoh fungsi Rust kompleks dan batas rustc saat ini
- Dalam contoh
do_thingyang menerimaOption<usize>,&dyn Context,&str,[char; 6], dan structOptions, setelah flattening dan pengurutan, semua argumen LLVM mentah dapat masuk ke register - Tipe argumen LLVM mentah pada contoh tersebut menjadi seperti berikut
gprs: i64, ptr, ptr, ptr, i64, i32, i32xmm0: i32, i32, i32, i32xmm1: i32, i1, i1, i1, i1
- Prolog fungsi mengambil primitive lalu merakit ulangnya menjadi nilai tingkat Rust
Option<usize>menjadi{ i64, i1 }- trait object menjadi
{ ptr, ptr } &strmenjadi{ ptr, i64 }[char; 6]menjadi[6 x i32]Optionsmenjadi{ i32, i1, i1, i1 }
- Jika metadata
!dbgditempelkan pada instruksi yang benar-benar me-materialize nilai argumen, gdb dapat memberikan hasil yang lebih baik saat menampilkan nilai argumen - Saat ini rustc mengirim 8 parameter seukuran pointer ke LLVM untuk fungsi yang sama, sehingga pada akhirnya semua 6 register integer dipakai dan 2 nilai dikirim lewat stack
Peluang optimisasi untuk nilai kembalian dan Result
- Desain ini tidak mencakup semua optimisasi konvensi pemanggilan yang mungkin dilakukan
- Dalam beberapa kasus, register tambahan seperti register AVX di x86 dapat dipakai
- Cara mengirim struct dengan membaginya antara register dan stack juga dapat dipertimbangkan
- Ada peluang optimisasi tersendiri untuk kembalian
Result- Jika melewati beberapa lapisan fungsi melalui
?, perpindahan register yang berulang bisa menjadi banyak - Jika
Resultterlalu besar untuk masuk register, setiap pemanggilan?harus memuat ok bit dari memori di stack untuk memeriksanya - Sebagai alternatif, error dapat diletakkan sebagai pointer out-parameter, sementara ok variant payload dan is-ok bit dikembalikan sebagai
Option<T> - Perlakuan detail saat
?disertai pemanggilanIntomemang rumit, tetapi dapat diimplementasikan
- Jika melewati beberapa lapisan fungsi melalui
ABI yang bergantung pada optimisasi
- Berbeda dari C, dalam
-Zcallconv=fast, Rust dapat melihat body fungsi saat membuat ABI yang akan dilihat pemanggil - Crate dapat mengiklankan ABI yang akurat dari sudut pandang pengiriman register untuk tiap fungsi
- Optimisasi paling sederhana adalah membuang argumen yang tidak dipakai dari ABI
- Jika sebuah fungsi tidak memakai parameter tertentu, register tidak dipakai untuk argumen tersebut
- Jika argumen
&Ttidak dipertahankan dan tidak dikonversi menjadi raw pointer, sertaTkecil danT: Freeze, pointee itu sendiri dapat dikirim by-value alih-alih referensinya - API seperti
HashMap::get()adalah kandidat- Jika key bertipe seperti
i32, saat ini integer harus di-spill ke stack dan pointernya diteruskan - Traffic memori ini dapat dihindari
- Jika key bertipe seperti
- ABI berbasis profil adalah bentuk yang lebih agresif
- Argumen yang lebih hot dapat diprioritaskan dalam urutan alokasi register
- Meski struct besar diterima lewat referensi, 3 field
i64yang hot dapat di-load terlebih dahulu oleh pemanggil dan dikirim lewat pointer sekaligus register - Callee toh harus melakukan load itu, jadi tidak ada biaya tambahan
- Instrumentation profile juga dapat membenarkan duplikasi fungsi yang hanya berbeda ABI
Mengapa ini belum dilakukan
- Dibanding C++, Rust memiliki batasan ABI yang lebih sedikit sehingga bisa menghasilkan kode yang lebih baik, dan gagasan ini bersinggungan dengan cara yang benar-benar dipakai oleh Go register ABI
- Hambatan pertama adalah kompleksitas pembuatan kode ABI
- LLVM hampir tidak menyediakan knob kendali yang berguna
- Di dalam rustc pun ini bukan area yang ramah
- Implementasi yang salah dapat berdampak buruk pada kegunaan
- Hambatan lain adalah kurangnya keahlian
- Di antara kontributor rustc, hanya sedikit yang cukup memahami semantik LLVM dan karakteristik pembuatan kode untuk menghasilkan kode yang baik tanpa membuat LLVM crash
- Waktu kompilasi juga dapat menjadi beban
- Semakin kompleks signature fungsi, semakin banyak kode prolog/epilog yang harus diproses LLVM
- Namun karena
-Zcallconvdimaksudkan hanya dipakai saat optimisasi aktif, ini tidak dianggap sebagai kelemahan yang menentukan
- Kode ABI Rust adalah area dengan bus factor rendah, dan pengetahuan LLVM dapat dimanfaatkan langsung untuk membantu tim compiler Rust membuat kode yang lebih teroptimasi
1 komentar
Opini Hacker News
Saat mengoptimalkan calling convention, intinya bukan membayangkan bentuk yang terasa bagus di kepala, melainkan mengukur performa
Kode itu bagus kalau memang cepat, bukan kalau terlihat cepat
Kadang apa yang disebut penulis sebagai kode buruk justru menjadi yang tercepat karena alasan yang sama sekali tidak intuitif, dan itu hanya bisa diketahui dengan mengukurnya pada benchmark besar
Salah satu alasan calling convention yang tampak buruk bisa bekerja dengan baik adalah karena ia menghemat register argumen, sehingga sedikit memudahkan register allocator
Selain itu, CPU masa kini dioptimalkan untuk alur instruksi yang dibuat compiler C, jadi khususnya jika menghasilkan kode ala compiler C seperti MSVC yang ternyata cukup sering meneruskan lewat stack, itu bisa cocok dengan titik optimal CPU
Karena inlining sudah sangat baik, pemanggilan pada jalur panas menjadi batas yang jarang, dan kalau batas itu agak berantakan pun tidak masalah selama membuat hal-hal lain lebih sederhana
Ini bukan berarti perubahan di sini buruk, tetapi aneh jika membahasnya tanpa pengukuran hanya karena kode terlihat aneh
Saya pernah bekerja mengoptimalkan calling convention di JavaScriptCore, dan dalam kode besar nyata, kode penerusan lewat stack yang tampak buruk ternyata menang dengan frekuensi yang mengejutkan
Namun menurut saya hasil pengukuran performa saja tidak boleh menjadi satu-satunya tolok ukur
Dalam ungkapan bahwa CPU “masa kini” telah dioptimalkan, kata pentingnya adalah masa kini; CPU terus berubah, jadi calling convention harus menjadi desain jangka panjang
Karena itu, sayangnya lebih menguntungkan untuk tidak terlalu menyimpang dari cara C++ melakukannya. Sebab optimasi prosesor ke depan kemungkinan besar juga akan menyasar ke arah itu
Pada saat yang sama, sebaiknya membuat calling convention kuat dan berorientasi masa depan dengan mempertimbangkan prinsip umum yang tidak mudah berubah, seperti menghemat register argumen
Rasanya agak aneh mengatakan ini, karena dalam beberapa tahun terakhir Rust tampaknya menjadi terlalu konservatif dalam hal toleransi terhadap keanehan (https://steveklabnik.com/writing/the-language-strangeness-bu...). Pada akhirnya, tanpa menjadi berbeda, kita tidak bisa menjadi lebih baik
Jika begitu fungsi dimulai ia langsung mengambil alamat parameter dan meneruskannya ke fungsi yang tidak dikenal, pada akhirnya parameter itu tetap harus ditumpahkan ke stack
Akan menarik melihat optimasi calling convention berbasis isi fungsi. Untuk fungsi statis di C, tampaknya aman selama alamatnya tidak diambil
JIT punya keunggulan dalam masalah ini karena bahkan sebelum membuat satu baris assembly, ia sudah mengumpulkan banyak informasi tentang CPU yang benar-benar sedang menjalankan kode
Pada kode hasil kompilasi statis murni, kumpulan fitur arsitektur saat runtime tidak diketahui, sehingga justru pada kode yang paling ingin dioptimalkan sering ditemui penghalang inlining
Saat ini Rust tampak lemah dalam hal ini di platform kecil, dan calling convention mungkin bisa membantu terkait pengembalian
ResultMeski begitu, saya penasaran apakah manfaat empiris dari penerusan lewat stack tetap berlaku ketika berpindah ke CPU ARMV8 yang punya banyak register, atau ke RISC-V
Draf ini masuk akal, tetapi tidak membedakan caller-saved/callee-saved, dan ada kesalahan umum berupa mengalokasikan sebagian register input untuk output.
Ekspektasi bahwa debugger akan memahami calling convention yang berbeda dari C juga terlalu optimistis. Apa pun yang bisa dienkode DWARF, dalam praktiknya kemungkinan besar akan gagal total.
Mengubah ABI bergantung pada pengaturan optimisasi akan berinteraksi sangat buruk dengan kompilasi terpisah.
Cara mengatur ulang argumen seperti bin packing memang akan bekerja, tetapi kompleksitas compiler meningkat besar, dan saya tidak yakin nilainya dibanding penempatan first-fit dari kiri ke kanan. Pengembang juga jadi lebih sulit memprediksi argumen akan ditempatkan di mana.
Arah besar untuk memiliki calling convention berbeda bagi fungsi yang alamatnya lolos keluar dan yang tidak memang tepat. Memisahkan prologue yang melakukan impedance matching juga bekerja dengan baik.
Rust memang seharusnya bersedia memiliki calling convention yang berbeda dari C, tetapi saya tidak yakin apakah itu harus berupa satu konvensi hardcoded yang dipakai semua fungsi. Memasukkannya ke type system tampak alami, dan jika pengembang bisa mengontrol calling convention, salah satu keunggulan performa assembly akan hilang.
Dari sisi caller, register output toh harus dikosongkan di antara dua pemanggilan fungsi, dan ini juga cukup luas dipakai dalam system calling convention.
Mungkin tujuannya agar callee lebih mudah menyiapkan nilai output sambil membiarkan nilai input tetap ada. Kalau begitu, saya bisa memahami penempatan register output di akhir urutan input untuk menghindari tumpang tindih, tetapi saya tidak begitu melihat alasan mengapa semua bentuk tumpang tindih harus sepenuhnya dilarang.
Function AmemanggilFunction B,Function C,Function Djuga ikut terhalang.Saya ragu semantik seperti apa yang bisa mempertahankan optimisasi semacam itu sekaligus mengizinkan kontrol; mungkin itu pada dasarnya hanya ilusi.
Dalam praktiknya, assembly tidak menjadi sasaran sebagian besar optimisasi compiler, sehingga ada kerugian performa. Sering kali ia juga tidak mendapat optimisasi seperti “memeriksa perilaku, menentukan bahwa itu sepenuhnya duplikat, lalu menghapusnya seluruhnya”, dan sekarang bukan lagi tahun 1990-an.
Namun jika kasusnya memang tidak memungkinkan optimisasi seperti itu dipertimbangkan sama sekali, menurut saya tempat inline assembly jelas kalah hanyalah sekitar profile-guided optimization. Alasannya, pengembang aplikasi memahami perilaku kode secara sempurna, sedangkan pengembang compiler tidak.
Overhead pemanggilan bisa dihilangkan dengan memakai lebih banyak assembly sampai batas panas yang terkait ikut tercakup.
bool, karena dapat membuat rantai dependensi.Di x64, untuk
bool, tampaknya tidak ada cara yang jelas lebih baik daripada memasukkannya dulu ke register, melakukan shift, lalu OR ke hasilnya.Cara sederhana akan membuat rantai dependensi sepanjang 64 dan bisa menimbulkan penalti 64 siklus, meski dengan cara yang baik mungkin bisa dikurangi menjadi 6 siklus, atau secara realistis sekitar 12 siklus.
Tetapi dari mana 64
boolitu berasal juga jadi masalah. Register tidak sebanyak itu, sehingga pada akhirnya harus dibaca lagi dari stack.Jika Rust ABI sudah mengepak
booldi dalam struct serapat ini, maka itu memang harus dilakukan juga, tetapi saya tidak begitu tahu.Lalu caller harus membongkar semuanya lagi.
Mengajari compiler untuk menumpahkan nilai ke ruang hasil di stack mungkin lebih mudah dan kemungkinan performanya juga lebih baik.
Kalau begitu, saya bertanya-tanya seberapa besar sebenarnya manfaat memasukkan nilai ke register.
Calling convention C memang agak kurang bagus.
Memang benar calling convention C tidak bisa diubah, tetapi itu tidak membuatnya jadi kurang disayangkan.
Semua register caller-saved yang tersedia seharusnya dipakai untuk argumen dan nilai balik, tetapi pada SysV ABI tradisional, hanya satu register, kadang dua, yang dipakai untuk nilai balik.
Jika mengembalikan
struct Point3D { long x, y, z },Point3Dsebenarnya bisa dimasukkan kerax,rdi,rsi, tetapi malah ditumpahkan ke stack.Sistem lain punya trik lain. Jika ingatan saya benar, di SBCL, ketika sebuah fungsi mengembalikan banyak nilai, ia menyetel carry flag saat keluar. Misalnya, rasanya bagus juga memakai carry flag untuk menandai apakah
Resultberisi error.Calling convention C pada dasarnya mendukung apa yang didukung C, yaitu mengembalikan satu argumen. Bahkan pengembalian struct pun tidak benar-benar baik.
Di C, ini mendekati “memangnya tidak tahu akan begitu”, sedangkan di C++ menjadi semacam “ya tinggal inline saja”.
Sebaliknya, spill ke memori benar-benar terjadi. Misalnya, ruang register dan window yang lapang pada SPARC meninggalkan banyak register yang tidak terpakai pada fungsi sederhana, dan men-spill ring register menyebabkan penggunaan stack besar yang merusak cache.
Di x86, meski ada banyak
movyang memindahkan data ke “tempat yang diperlukan”, hasil akhirnya sering lebih cepat.Jika hanya melihat kode callee, kita tergoda mengatakan “argumen ini di sini, nilai balik itu di sana pasti lebih cepat”, tetapi caller tidak diketahui.
Tidak ada jaminan bahwa penyiapan argumen akan diteruskan begitu saja, atau nilai balik akan dikonsumsi secara panas. Misalnya, jika
struct Point { x: i32, y: i32, z: i32 }dipakai sebagai argumen/nilai balik dan caller dalam loop melakukan sesuatu sepertimystruct.deepinside.point[i] = func(mystruct.deepinside.point[i]), memasukkan dan mengeluarkannya lewat register bisa menjadi overhead atau bahkan menghalangi vektorisasi.Callee tidak bisa mengetahui ini, kecuali ketika compiler bisa melihat kedua sisi dan melakukan inline.
Buah yang paling mudah dipetik terkait pemanggilan tampaknya adalah menghapus asumsi yang tertanam di hampir semua ABI C bahwa fungsi mengembalikan satu nilai primitif. Sisanya membutuhkan banyak benchmark dan statistik code generation.
Rust memiliki detail lain yang disayangkan yang membuat struct menjadi lebih besar dari yang diinginkan
Bayangkan struct
Fooyang berisi 8 fieldOptionyang nilainyaNoneatauSome(u8). Di C, ini bisa direpresentasikan dengan 8 buahbool1-bit dan 8 buahuint8_t, total 9 byteDi Rust, diskriminan 1 byte dan
uint8_tdiulang 8 kali, sehingga menjadi 16 byteAlasannya adalah struct harus bisa menyediakan peminjaman field. Jika ada
&Foo, compiler harus bisa membuat&Foo::some_field, yaitu&Option, dan&Optionini harus memiliki bentuk yang sama dengan semua&Optionlain di dalam programKarena itu,
Optioninternal harus memiliki layout yang sama denganOptionlain di program, yaitu bit diskriminannya sendiri yang dibulatkan ke byte ditambahu8. Meski&Foo::some_fieldsebenarnya tidak pernah dibuat, struct tetap membayar biaya iniJika mempertimbangkan
Optiondari tipe yang lebih besar, kondisinya makin buruk. Pada struct dengan 8 fieldOption, tiap diskriminan dibulatkan menjadi 2 byte sehingga totalnya 32 byte, dan seperempatnya—atau hampir setengah jika termasuk bit diskriminan yang tidak terpakai—terbuang sebagai padding di tengah. Struktur ekuivalen di C cukup 18 byteDengan
Option, struct Rust bisa menjadi 128 byte, sedangkan struct C 72 byteTentu saja, kita bisa mengimplementasikan representasi seperti C dengan menaruh satu
u8untuk diskriminan yang dipadatkan dan 8 buahMaybeUninit, lalu membuat sendiri fungsi-fungsi yang memetakan dari&FookeOption<&T>dan dari&mut FookeOption<&mut T>. Namun itu tidak bisa menjadi&Optionatau&mut Optionhttps://play.rust-lang.org/?version=stable&mode=debug&editio...
Pada dasarnya yang dijelaskan adalah tipe buatan pengguna yang berisi 8
Option, dan begitu mulai memikirkan performa, penangananOptioninternal memang harus dikelola sendiriSulit menyebut ini sebagai kekurangan hanya karena Rust menyediakan fitur nyaman yang bisa dipilih ketika sesuai dengan tujuannya
Kasus penggunaan yang dijelaskan relatif jarang, dan jika itu benar-benar bottleneck performa, meluangkan sedikit waktu ekstra untuk mengimplementasikannya di Rust bukan masalah besar
Karena manfaat tipe
Option<_>dalam penggunaan umum sangat besar, sulit menganggap ini sebagai “detail yang disayangkan” dari RustAda bagian yang mengatakan bahwa jika alamat fungsi non-polimorfik dan non-inline bisa diambil sebagai function pointer, maka dibuat shim yang memakai
-Zcallconv=legacylalu langsung melakukan tail call ke implementasi sebenarnya. Saya paham niatnya untuk menjaga kesetaraan function pointerNamun jika shim legacy melakukan tail call ke fungsi dengan calling convention Rust, bukankah itu tidak bisa memperbaiki perbedaan nilai kembalian dari calling convention?
Sedikit berbeda, tetapi saya penasaran apakah saat ini interoperabilitas Go dan Rust memungkinkan
Dulu saya ingat pernah melihat contoh yang berhasil dengan Zig di tengah, tetapi tidak bisa menemukannya. Saya punya kode Rust legacy dan ingin memindahkannya sedikit demi sedikit ke Go
extern "C"untuk memanggil fungsi RustSaya mempresentasikan cara pemakaiannya dengan GitHub code search di RustConf 2023 (https://www.youtube.com/watch?v=KYdlqhb267c), dan setelah itu mendengar bahwa tempat seperti 1Password juga melakukan hal serupa
Memindahkan tipe melewati batas interoperabilitas C itu merepotkan sehingga tidak menyenangkan, tetapi memungkinkan dan kode bisa digunakan ulang
extern "C", lalu panggil dari Go seperti memanggil CUntuk arah sebaliknya saya kurang tahu
Kode terkelola harus bisa memiliki memori yang akan dibebaskan atau dipindahkan, sedangkan kode tidak terkelola harus menalar kapan memori dibebaskan atau dipindahkan
Hal seperti
cgomemungkinkan pemanggilan FFI dari kode terkelola Go ke memori tidak terkelola, tetapi ada biayanyaDalam implementasi bahasa yang saling memanggil tetapi tidak berbagi garbage collector, masalah ini selalu muncul
Mencampur kode terkelola/tidak terkelola adalah ide lama sekaligus topik yang masih aktif diteliti
Kecuali runtime bawaan memang dirancang untuk itu, memanggil kode terkelola dari kode tidak terkelola hampir selalu merupakan ide buruk, dan biasanya ada lapisan serialisasi di tengah
Kalau ini pekerjaan utama, mungkin saya akan menganggapnya buruk, tetapi saya lelah karena setiap kembali ke kode setelah beberapa minggu, saya tidak ingat lagi apa yang dilakukan dan bagaimana caranya
Saya meneruskan closure Rust yang memiliki state sebagai callback ke kode Go untuk dimasukkan ke fungsi standard library Go, bahkan termasuk unwinding panic di dalam closure Rust
https://github.com/Voultapher/sort-research-rs/commit/df6c91...
Saya cukup lama memakai Inspect Element untuk mencari tahu bagaimana judul bagian dibuat miring, tetapi dengan tool Safari saya mentok. Sebenarnya bagaimana caranya?
.post-title:transform: skewY(-2deg) translate(-1rem, -0.4rem);element()(https://developer.mozilla.org/en-US/docs/Web/CSS/element), tetapi ternyata sebenarnya itu salinan isi artikel yang diperkecil sangat kecilh1, h2, h3, h4, h5, h6diterapkantransform:skewY(-2deg) translate(-1rem,0rem);,transform-origin:top;,font-style:italic;,text-decoration-line:underline;,text-decoration-color:goldenrod;,text-underline-offset:4%;,text-decoration-thickness:.25exSebagai kontras, ada artikel tahun 2019 “How Swift Achieved Dynamic Linking Where Rust Couldn't”
https://faultlore.com/blah/swift-abi/
Memang disayangkan Rust belum memiliki calling convention untuk semantik tingkat Rust, tetapi pada saat yang sama artikel itu menunjukkan betapa besarnya pekerjaan yang diperlukan untuk sampai ke sana
Apple punya motivasi kuat untuk menjadikan Swift bahasa sistem praktis yang bisa diandalkan aplikasi, tetapi Rust tidak punya sponsor semacam itu
Diskusi HN: https://news.ycombinator.com/item?id=21488415
Akan bagus jika Rust punya lebih banyak opsi dukungan untuk trade-off ini, dan tidak harus terbatas hanya pada hal seperti https://github.com/rust-lang/rfcs/pull/3470
Jika compiler Rust saat ini melakukan inline secara agresif lalu mengoptimalkan, saya ragu apakah ini sepadan dengan usahanya
Jika fungsi yang dipanggil kecil, ia akan di-inline; jika besar, waktu akan cukup banyak dihabiskan di dalam fungsi, sehingga overhead pemanggilannya kecil
dyn Trait, tidak bisa di-inline, jadi perubahan seperti ini membantuJika pemanggilan bisa dibuat murah, inline tidak perlu dilakukan seagresif itu, sehingga juga bisa membantu ukuran kode dan waktu kompilasi
Fungsi kompleks yang tidak cocok untuk inline kemungkinan besar akan mengakses memori beberapa kali, dan akses itu kemungkinan menjadi bottleneck
Pengiriman lewat stack makin memperketat bottleneck itu, karena menambah tekanan cache serta load/store
Jika Rust bisa mengirim argumen secara optimal pada proporsi pemanggilan fungsi yang signifikan, bukan hanya beberapa siklus akses L1 yang bisa dihindari, tetapi CPU juga mungkin bisa lebih cepat mencapai bottleneck memori yang esensial
Mungkin ada keuntungan beberapa persen, tetapi sekarang saya sedang minum wine dan tidak sedang menghitung
Bisa jelaskan apa mnemonik “Diana’s silk dress cost $89” yang muncul di referensi x86?