Tree Borrows: Model Aturan Aliasing untuk Kode `unsafe` Rust
(plf.inf.ethz.ch)- Agar kompiler Rust dapat memanfaatkan jaminan aliasing pointer untuk optimasi, perlu ada definisi yang jelas tentang di mana kode unsafe melanggar aturan
- Stacked Borrows yang ada sebelumnya memberikan tolok ukur tersebut, tetapi belum cukup mengakomodasi pola umum dalam kode unsafe Rust di dunia nyata maupun fitur terbaru borrow checker
- Tree Borrows mengubah struktur inti Stacked Borrows dari stack menjadi tree sehingga dapat merepresentasikan lebih banyak pola yang valid
- Dalam evaluasi terhadap 30.000 crate Rust yang paling banyak digunakan, jumlah test case yang ditolak 54% lebih sedikit dibandingkan Stacked Borrows
- Pembuktian dengan Rocq mengonfirmasi bahwa sebagian besar optimasi yang ada tetap dipertahankan, sambil memungkinkan optimasi baru seperti read-read reordering
Aturan aliasing yang dibutuhkan di unsafe Rust
- Rust memberikan jaminan kuat seperti keamanan memori dan pencegahan data race melalui sistem tipe berbasis ownership
- Namun, di area kode unsafe, keamanan tidak dijamin secara otomatis, sehingga diperlukan aturan terpisah yang harus dipatuhi programmer
- Kompiler berupaya memperkuat optimasi di dalam fungsi dengan memanfaatkan jaminan dari sistem tipe, khususnya informasi terkait aliasing pointer
- Kode unsafe yang ditulis keliru dapat merusak optimasi ini, sehingga penting untuk memiliki kriteria yang jelas tentang kode mana yang dianggap “badly behaved”
- Riset sebelumnya, Stacked Borrows, mendefinisikan kriteria ini tetapi memiliki keterbatasan
- Menolak berbagai pola yang umum ditemukan dalam kode unsafe Rust nyata
- Tidak mencerminkan fitur lanjutan borrow checker Rust yang baru diperkenalkan
Pendekatan Tree Borrows dan hasil evaluasinya
- Tree Borrows didefinisikan dengan mengganti stack, yang merupakan struktur pusat Stacked Borrows, menjadi tree
- Perubahan struktur ini melonggarkan batasan model sebelumnya
- Dalam evaluasi terhadap 30.000 crate Rust yang paling banyak digunakan, jumlah test case yang ditolak berkurang 54% dibandingkan Stacked Borrows
- Pembuktian dengan Rocq juga memverifikasi sifat-sifat terkait optimasi
- Mempertahankan sebagian besar optimasi yang diizinkan oleh Stacked Borrows
- Juga memungkinkan optimasi baru yang penting, yaitu read-read reorderings
- Tree Borrows menerima PLDI'25 Distinguished Paper Award
- Materi terkait
1 komentar
Komentar Hacker News
Sebagai bonus, ada juga presentasi terbaru dari grup Ralf Jung yang mencoba menspesifikasikan secara presisi semantik eksekusi Rust dalam bentuk yang dapat dijalankan, menggunakan dialek Rust: https://youtube.com/watch?v=yoeuW_dSe0o
Torvalds sudah lama berpendapat bahwa strict aliasing rules di C lebih banyak mudaratnya daripada manfaatnya, dan itu terdengar meyakinkan. Contohnya di sini: https://lore.kernel.org/all/CAHk-=wgq1DvgNVoodk7JKc6BuU1m9Un... Kalau tertarik pada topik ini, seluruh thread-nya juga layak dibaca
Dari pengalaman saya yang terbatas, Rust tampaknya tidak berbeda secara mendasar. Setidaknya makin begitu jika unsafe ikut terlibat
Menurut saya ini lebih berguna bagi compiler dan tidak terlalu membebani programmer. Selain itu, di dalam bahasanya benar-benar ada jalan keluar: gunakan raw pointer. Dan ada juga alat untuk memeriksa kode
Pada akhirnya, seperti semua hal dalam desain bahasa, ini adalah kompromi, dan menurut saya Rust mungkin telah menemukan sweet spot baru untuk optimisasi semacam ini. Waktu yang akan membuktikan apakah itu benar
C punya perangkat seperti bom nuklir bernama
restrict, yang dari pengalaman saya hanya benar-benar berdampak di clang dan gcc ketika ditempelkan pada argumen fungsi. Type-based alias analysis umumnya sulit digunakan, dan Anda tidak bisa membuat salinan tipeint64_ttanpa batas, juga kemungkinan tidak ingin melakukannya. Keharusan memakaimemcpyuntuk menafsirkan ulang sebagai tipe lain juga menyebalkanSebaliknya, referensi Rust memiliki batasan lifetime, scope, dan mutability yang terperinci, dan tidak terlalu peduli pada tipe “fisik” itu sendiri. Jadi, dimungkinkan juga untuk menafsirkan ulang dan beralih pada memori yang sama sebagai
&mut i32/&i32dan&mut i64/&i64. Selama abstraksi unsafe tidak memberikan referensi&mutyang tumpang tindih secara bersamaan, atau selama satu&mutdibagi menjadi beberapa&mutyang tidak tumpang tindih, Anda bisa membaca dan menulis setengah nilai atau beberapa nilai dengan operasi baca-tulis Rust aman yang biasaAlias analysis sangat penting untuk mendapatkan performa yang baik saat ini. Namun perlu diingat juga bahwa manfaat terbesar datang dari heuristic yang paling sederhana. Misalnya, dua load yang menggunakan nilai SSA yang sama sebagai pointer pasti saling alias
Dari sudut pandang LLVM, BasicAA menjalankan peran itu. Ini adalah kumpulan heuristic sederhana yang kurang lebih berbunyi “jika titik alokasi objek bisa dilacak, selesaikan alias query secara pasti; jika tidak, tidak tahu”
Pertanyaan sebenarnya adalah nilai alias analysis yang melampaui pemeriksaan dasar dan jelas. Ketika alias query tidak lagi bisa diselesaikan secara trivial, hal yang bisa dilakukan dari hasilnya pun biasanya sangat berkurang, dan hampir hanya sebatas menemukan risiko code motion. Manfaatnya jauh lebih kecil
Salah satu eksperimen yang ingin saya coba adalah mengukur total peningkatan kecepatan yang akan diberikan oleh alias analysis yang secara teoretis sempurna. Dugaan saya, bahkan pada kode non-HPC seperti kernel Linux pun angkanya sekitar 20%
[1] Ini tidak mencakup optimisasi heroik seperti transformasi tata letak data yang tidak akan dicoba tanpa alias analysis berkualitas tinggi. Karena kita sudah tahu bahwa alias analysis seperti itu pada praktiknya tidak ada, optimisasi semacam itu juga tidak akan dicoba, dan menurut saya tidak layak dimasukkan ke estimasi peningkatan kecepatan
Aliasing C hanya berbasis tipe, sehingga nama lainnya adalah type-based alias analysis atau TBAA
Saya menemukan klaim bahwa
noaliasberkontribusi pada peningkatan performa sekitar 5% berdasarkan waktu eksekusi, tetapi jelas datanya sudah sangat lamahttps://github.com/rust-lang/rust/issues/54878#issuecomment-...
https://news.ycombinator.com/item?id=22281205
https://news.ycombinator.com/item?id=17715399
Penjelasannya terlihat seperti: jika membuat
*mut i32dari&mut, lalu melakukan*x = 10alih-alihwrite(x), maka compiler semestinya menolaknya karena tidak memakai implicit two-phase borrow, tetapi pada praktiknya lolos*x = 10;melaporkan error, sedangkan versiwrite(x);tidakError-nya berbentuk “Undefined Behavior: attempting a write access using [...] but that tag does not exist in the borrow stack for this location”
rustc sendiri tidak punya alasan untuk menolak salah satu dari keduanya.
yadalah*mut, dan dari sudut pandang sistem tipe pada waktu kompilasi, ia tidak memiliki hubungan peminjaman atau lifetime dengan&mutyaituxBorrow checker saat ini menggunakan analisis yang lebih restriktif, sehingga tidak dapat mendeteksi konflik spesifik ini antara pointer mentah dan referensi mutable
Berdasarkan pengalaman nyata [1] [2], ia juga mengizinkan kode yang masuk akal tetapi ilegal di Stacked Borrows
[1] https://github.com/Voultapher/sort-research-rs/blob/main/wri... kolom Miri
[2] https://github.com/rust-lang/rust/blob/6b3ae3f6e45a33c2d95fa...
Rust telah berpindah dari borrow checker berbasis scope ke borrow checker dengan lifetime non-leksikal, dan implementasi eksperimental Polonius berikutnya juga tersedia sebagai opsi. Namun ketika implementasi baru siap untuk produksi, implementasi lama ditinggalkan. Sebab tidak ada alasan untuk memilihnya
Pemeriksaan peminjaman itu cepat, dan implementasi baru menerima secara ketat lebih banyak program yang benar
Selain itu ada tipe
RcdanRefCell, yang memungkinkan fleksibilitas lebih besar dengan membayar biaya pemeriksaan runtimeSemuanya memiliki biaya dan kemampuan berbeda dalam hal implementasi, performa, dan pengalaman developer
Dan yang sebenarnya dituju oleh kebanyakan bahasa selain Rust adalah produktivitas dari manajemen sumber daya otomatis. Apa pun metodenya, mereka memakai manajemen sumber daya otomatis, lalu menggabungkannya dengan salah satu sistem tipe di atas hanya pada jalur yang kritis terhadap performa
Dalam konteks ini, “Rust” bisa dilihat tidak lebih dari “invariant yang biasanya diinginkan orang” dan “sekumpulan optimisasi yang mengasumsikan invariant umum itu, tidak lebih dan tidak kurang”
Sebagian besar waktu kompilasi dipakai untuk resolusi trait, monomorfisasi, pass optimisasi LLVM, dan linking
Mungkin ini pertanyaan bodoh, tetapi saya penasaran apakah tidak bisa menjalankan beberapa implementasi di thread paralel dan membiarkan yang pertama memberikan hasil positif menjadi pemenangnya
Menggunakan pointer untuk membuat beberapa referensi mutable terhadap variabel yang sama ada secara bersamaan adalah perilaku tak terdefinisi. Kecuali saya salah memahami maksud makalahnya, begitulah kelihatannya
Kode di atas diterima oleh compiler Rust, tetapi melanggar aturan. Pertanyaannya adalah aturan mana yang dilanggar
Pada dasarnya, apa yang diterima borrow checker itu legal, unsafe dapat mengekspresikan hal yang ilegal atau perilaku tak terdefinisi, dan ada himpunan aturan yang lebih luas daripada yang dapat diperiksa borrow checker tetapi tetap legal dan merupakan perilaku terdefinisi
Tujuan riset ini adalah menspesifikasikan himpunan aturan itu secara presisi. Garis besarnya kurang lebih “pointer yang dapat ditulis tidak boleh beralias”, tetapi detail seperti pointer internal, invalidasi iterator, dan apakah yang bermasalah itu membuat pointer buruk atau menggunakannya, sangat sulit
Makalah Stacked Borrows sebelumnya lebih sederhana tetapi lebih restriktif, sehingga kode unsafe di dunia nyata sering gagal melewati aturannya. Tree Borrows lebih luas dan mengizinkan lebih banyak kode, sambil tetap aman secara dapat dibuktikan
Tree Borrows justru mengusulkan definisi semacam itu
Di sini, “kode dapat melakukan hal seperti ini” berarti “Anda dapat menulis, mengompilasi, dan menjalankan kode ini, dan tanpa sesuatu seperti Tree Borrows, tidak ada dasar untuk menyatakan bahwa kode ini bermasalah”
Anda pada dasarnya sudah menerima bahwa kode seperti ini harus dikatakan sebagai perilaku tak terdefinisi, yaitu bahwa sesuatu seperti Tree Borrows diperlukan. Bagian makalah ini adalah bagian yang berargumen mengapa hal semacam itu diperlukan
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Isinya mengatakan bahwa karena para pengembang compiler Rust jelas ingin mendukung optimisasi aliasing, diperlukan cara untuk “mengecualikan” contoh kontra seperti di atas dari pertimbangan
unsafedimaksudkan untuk kasus ketika sulit membuktikan validitas kode melalui analisis lifetime Rust, tetapi bisa disalahgunakan untuk melakukan jauh lebih banyak hal daripada itu