- Paralelisasi compiler Rust selama ini terutama bergantung pada Cargo dan backend LLVM, tetapi kini eksekusi frontend paralel juga ditambahkan sehingga bottleneck di paruh akhir build dapat dikurangi
- Di nightly, fitur eksperimental ini dapat diaktifkan dengan
-Z threads=8, dan default-nya masih mode single-thread, sehingga tidak ada peningkatan kecepatan tanpa pengaturan eksplisit
- Frontend baru berbasis Rayon membagi pekerjaan kompilasi secara granular, dan dalam pengukuran contoh waktu frontend berkurang dari 10,2 detik menjadi 5,9 detik
- Dalam pengukuran kode nyata, waktu kompilasi turun hingga 50%, tetapi variasinya besar tergantung karakteristik kode dan konfigurasi build, sementara penggunaan memori dapat meningkat hingga 35%
- Fitur ini masih dalam tahap eksperimen, dengan target 2024 untuk stabilisasi
-Z threads dan eksekusi multi-thread secara default di stable
Poros baru dalam paralelisasi kompilasi Rust
- Frontend compiler Rust kini dapat mengurangi waktu kompilasi melalui eksekusi paralel
- Pada compiler nightly, opsi
-Z threads=8 memungkinkan Anda mencoba frontend multi-thread
- Fitur ini masih eksperimental, dan target waktu untuk memasukkannya ke compiler stable adalah 2024
Optimasi yang sudah ada dan bottleneck yang tersisa
- Compiler Performance Working Group telah meningkatkan performa compiler Rust selama beberapa tahun
- Selama 10 bulan pertama 2023, waktu kompilasi rata-rata berdasarkan alat pengukuran performa turun 13%
- Penggunaan memori puncak turun 15%
- Ukuran biner mengecil 7%
- Karena compiler sudah banyak dioptimalkan, ruang peningkatan besar yang tersisa lebih dekat ke perluasan paralelisme
Batas paralelisasi Cargo dan backend
- Saat membuild program Rust, Cargo menjalankan beberapa proses
rustc untuk mengompilasi crate secara paralel
- Jika paralelisasi ini dimatikan dengan flag
-j1, waktu kompilasi program Rust besar akan meningkat tajam
- Flag
--timings milik Cargo menghasilkan chart timeline kompilasi crate
- Pada contoh build ripgrep di mesin dengan 28 core virtual, terlihat 60 baris proses
- Sebagian besar adalah
rustc, dan sebagian adalah build script
- 20 proses awal tidak memiliki dependensi antarkrate sehingga dapat dimulai bersamaan
- Semakin mendekati akhir build, dependensi crate bertambah dan paralelisme berkurang
- Dengan pipelined compilation, kompilasi crate dependensi dapat ditumpangtindihkan sampai tingkat tertentu, tetapi pada program Rust besar, eksekusi paralel di paruh akhir build jauh lebih sedikit
Tugas frontend dan backend
- Compiler Rust secara garis besar terbagi menjadi frontend dan backend
- Frontend melakukan parsing, type checking, borrow checking, dan sebagainya
- Frontend lama tidak dapat menggunakan eksekusi paralel
- Backend bertanggung jawab atas code generation; ia menghasilkan kode dalam satuan “codegen units”, lalu LLVM memprosesnya secara paralel
- Jumlah codegen unit default untuk release build adalah 16, sehingga pada profil contoh terlihat 16 thread LLVM
Bottleneck yang terlihat pada profil backend lama
- Dalam contoh pengukuran dengan Samply saat membuild crate terakhir Cargo dalam mode release, frontend memakan waktu 10,2 detik
- Backend memakan waktu 6,2 detik, dan thread LLVM berjalan selama 5,9 detik dari waktu tersebut
- Code generation paralel berbasis LLVM efektif, tetapi bahkan pada mesin 28 core, tidak semua 16 thread LLVM berjalan bersamaan
- Main thread menjalankan pekerjaan konversi MIR ke LLVM IR secara serial, sehingga muncul bentuk bertangga di bagian awal thread codegen
- Frontend yang sebelumnya sepenuhnya serial tetap menjadi titik peningkatan terbesar
Cara implementasi frontend paralel baru
- Frontend baru menggunakan Rayon untuk menjalankan pekerjaan kompilasi paralel yang granular
- Berbagai struktur data disinkronkan dengan mutex dan read-write lock, dan atomic type digunakan di tempat yang diperlukan
- Banyak pekerjaan frontend telah diparalelkan, tetapi perubahan terkonsentrasi pada sejumlah titik inti yang relatif kecil
- Sebagian besar kode frontend tidak perlu diubah
Hasil pengukuran dengan konfigurasi 8 thread
- Pada contoh yang sama dengan frontend paralel diaktifkan dan 8 thread digunakan, waktu eksekusi frontend turun dari 10,2 detik menjadi 5,9 detik
- Waktu eksekusi backend turun dari 6,2 detik menjadi 5,3 detik, dan waktu eksekusi thread LLVM turun dari 5,9 detik menjadi 4,9 detik
- Di frontend, 7 thread tambahan yang ditandai sebagai
rustc berjalan
- Pemanfaatan thread tidak merata dan semua 8 thread memiliki periode tidak aktif, sehingga masih ada ruang untuk peningkatan lebih lanjut
- Alasan 8 thread LLVM mulai bersamaan adalah karena 8 thread
rustc menghasilkan LLVM IR untuk 8 codegen unit secara paralel
- Jika jumlah thread frontend diubah menjadi 16, bentuk bertangga hilang sepenuhnya, tetapi waktu eksekusi akhir pada kasus tersebut hampir tidak berubah
Kombinasi paralelisasi antaraproses dan dalam proses
- Kompilasi Rust sejak lama diuntungkan oleh paralelisasi antaraproses dari Cargo dan paralelisasi dalam proses pada backend
- Kini frontend juga dapat memanfaatkan keuntungan paralelisasi dalam proses
- Saat beberapa proses
rustc berjalan bersamaan dan tiap proses membuat beberapa thread, jobserver protocol membatasi jumlah thread
- Jika paralelisme antaraproses tinggi, paralelisme dalam proses dikurangi menyesuaikannya, dan total jumlah thread tidak melebihi jumlah core
Cara penggunaan
- Compiler nightly sudah menyertakan frontend paralel
- Default-nya adalah mode single-thread, sehingga waktu kompilasi tidak berkurang begitu saja
- Mode multi-thread harus diaktifkan secara eksplisit dengan opsi
-Z threads
RUSTFLAGS="-Z threads=8" cargo build --release
- Untuk mengaturnya dengan
config.toml pada satu atau beberapa proyek, tambahkan berikut ini
[build]
rustflags = ["-Z", "threads=8"]
- Alasan mode single-thread menjadi default adalah demi peluncuran yang hati-hati
- Frontend paralel memiliki banyak kode baru
- Mode single-thread menjalankan sebagian besar kode baru, tetapi mengecualikan kemungkinan bug threading seperti deadlock
- Program paralel, bahkan di Rust, lebih sulit ditulis dengan benar dibanding program serial
- Karena itu, frontend paralel tidak akan disertakan dalam rilis beta atau stable untuk sementara waktu
Dampak performa dan memori
- Dalam mode single-thread, frontend paralel umumnya 0%–2% lebih lambat daripada frontend serial lama
- Dalam mode multi-thread
-Z threads=8, pengukuran kode nyata menunjukkan waktu kompilasi dapat berkurang hingga 50%
- Efek performa sangat bervariasi tergantung karakteristik kode dan konfigurasi build
- Development build kemungkinan melihat peningkatan yang lebih besar daripada release build
- Ini karena release build biasanya menghabiskan lebih banyak waktu untuk optimasi backend
- Pada sebagian program kecil yang sudah cepat dikompilasi, mode multi-thread bisa lebih lambat daripada mode single-thread
- Nilai yang direkomendasikan adalah 8 thread
- Ini adalah konfigurasi yang paling banyak diuji dan diketahui memberikan hasil yang baik
- Nilai lebih rendah dari 8 memberi manfaat lebih kecil, tetapi cocok untuk hardware dengan core kurang dari 8
- Nilai lebih tinggi dari 8 mengalami diminishing returns dan bahkan dapat memperburuk performa
- Alasan peningkatan dari 1 ke 8 thread hanya sekitar 50% adalah karena frontend hanya sebagian dari total waktu kompilasi dan backend sudah diparalelkan
- Dalam mode multi-thread, penggunaan memori dapat meningkat besar, dan peningkatan hingga 35% telah diamati
Ketepatan dan umpan balik
- Reliabilitas mode single-thread diperkirakan tinggi
- Mode multi-thread memiliki bug yang diketahui, termasuk deadlock
- Jika kompilasi macet, kemungkinan Anda menjumpai salah satu bug yang diketahui
- Apa pun frontend yang digunakan, biner yang dihasilkan compiler seharusnya sama; jika ada perbedaan, itu dianggap bug
- Jika ada masalah, periksa lebih dulu issue berlabel
WG-compiler-parallel, lalu buka issue baru jika tidak ada issue yang cocok
- Umpan balik umum dapat diberikan di wg-parallel-rustc Zulip channel, dan mereka terutama tertarik pada dampak performa pada kode nyata
Target stable pada 2024
- Pekerjaan peningkatan performa frontend paralel sedang berlangsung
- Seperti terlihat pada profil, tingkat pemanfaatan thread frontend masih memiliki ruang untuk ditingkatkan
- Bug yang tersisa pada mode multi-thread juga sedang dibereskan
- Target waktu untuk stabilisasi opsi
-Z threads dan penyediaan frontend paralel sebagai default multi-thread di rilis stable adalah 2024
1 komentar
Opini Hacker News
Saya tahu ini masih tahap awal, tetapi menurut saya kelemahan Rust adalah kecepatan kompilasi
Saat pernah bekerja di monorepo Rust, keluhan terbesar saya adalah kecepatan kompilasi; itu menaikkan biaya CI/CD, dan ketika cache harus dihapus, waktu pengembangan juga melambat drastis
Penyebabnya memang bug Docker, bukan Cargo, tetapi kemajuan seperti ini tetap menggembirakan
Sudah banyak dioptimalkan, dan compiler Rust saat ini punya paralelisme lebih tinggi daripada hampir semua compiler arus utama
Desain bahasa Rust sendiri membuat kompilasi lebih sulit dibanding bahasa seperti Go yang memang dibuat dengan tujuan kompilasi cepat
Saya tidak tahu seberapa langsung pekerjaan ini bisa diterapkan di sana, tetapi semoga ada peningkatan besar juga di sana
Itu jelas diperlukan untuk dukungan IDE modern
Saya maintainer proyek Rust open source ukuran menengah [1], dan menurut saya waktu kompilasi Rust secara lokal selalu terasa mengejutkan cepat
Di MacBook Pro, build debug hanya hitungan detik; build release dan CI/CD memang lambat, tetapi sejak mulai memakai Rust dua tahun lalu, kompilasi Rust terasa sangat cepat
Agar berimbang, pekerjaan utama saya adalah Java/Kotlin dan Gradle, dan di sana kita benar-benar bisa bicara soal waktu kompilasi yang seperti gletser
Di proyek Rust open source saya, saya meminimalkan dependensi, tidak memakai macro selain hal seperti
derive[Debug, Clone], dan sangat menahan penggunaan genericAkan menarik kalau Anda mencoba membangun proyek ini dengan
cargo builddan memberi masukan soal waktu kompilasinya[1]: https://github.com/Orange-OpenSource/hurl
Dan juga apakah proyeknya dipecah menjadi beberapa crate pada titik yang tepat
Berapa menit yang dibutuhkan untuk build monorepo?
Boleh bicara sejujur mungkin. Compiler utama yang saya pakai adalah GHC, jadi saya tidak gampang kaget
Mungkin ini pertanyaan bodoh, tetapi apakah backend harus menunggu frontend selesai melakukan borrow checking? Kalau ya, kenapa?
Bukan berarti ada yang salah, saya hanya penasaran apakah borrow checking menetapkan invariant yang diandalkan backend, lebih dari sekadar pemeriksaan konsistensi
Misalnya, apakah ada alasan mengapa backend tidak bisa melakukan pekerjaan spekulatif yang bisa dibuang jika terjadi error borrow checking
Namun mungkin saja itu tidak menghasilkan kode yang paling optimal
Setahu saya ada optimisasi yang memakai informasi yang ditetapkan selama borrow checking, seperti optimisasi
noaliasyang terkenal. Optimisasi ini perlu beberapa kali percobaan sebelum akhirnya diaktifkan[1]Saya juga tidak yakin hubungannya dengan NLL (non-lexical lifetimes), tetapi untuk menetapkan informasi yang dipedulikan backend, sepertinya setidaknya dibutuhkan borrow checker yang primitif
Namun mrustc juga mengompilasi versi Rust yang punya fitur NLL tanpa borrow checker, jadi tampaknya ini lebih dekat ke optimisasi daripada sesuatu yang wajib
[0]: https://github.com/thepowersgang/mrustc
[1]: https://stackoverflow.com/a/57259339
Apakah ada cara untuk menggunakan jumlah core CPU, alih-alih menanam nilai tetap di file konfigurasi yang dipakai di mesin berbeda?
Saya memperkirakan nilai default yang sudah distabilkan nanti adalah jumlah core
Saya tidak tahu pekerjaan ini sekarang sudah sampai mana, tetapi pada suatu waktu mereka mencoba mengoordinasikan pemanggilan rustc oleh Cargo lewat jobserver; jika begitu, ia akan memakai jumlah job Cargo yang default-nya adalah jumlah core
Cargo juga mendukung nilai negatif untuk mengurangi dari jumlah core
RUSTFLAGS, dan itu juga disebutkan di artikel:Bagus! Dulu sekali saat saya memakai Rust, bahkan contoh mainan pun kompilasinya cukup lambat, tetapi belakangan ketika kembali mencobanya, Rust benar-benar sudah membaik dan saya memakainya di mana pun memungkinkan tanpa terlalu memikirkan waktu kompilasi
Namun di satu proyek yang agak membesar, perubahan sederhana pun mulai butuh lebih dari 5 detik untuk dikompilasi, dan ingatan lama kembali muncul
Saya bahkan sampai ingin menunda penyimpanan agar analyzer tidak berjalan sebelum saya membereskan hal lain, sebelum laptop berputar seperti mesin pesawat
Secara pribadi ini titik sakit terbesar saya, jadi kemajuan apa pun sangat saya sambut
Bagus! Tidak seperti ekosistem crate library, crate binary saya pada dasarnya cenderung besar dan monolitik
Sekarang saya sedang memecahnya menjadi beberapa crate library
Artinya, bukan hanya tahap akhir kompilasi tidak bisa diparalelkan, tetapi crate-crate terbesar juga diproses secara berurutan, jadi perubahan ini sangat menggembirakan
Setelah beberapa tahun agak menjauh dari Rust dan bekerja di lingkungan seperti Python atau TypeScript, baru-baru ini saya memakainya lagi untuk sebuah proyek, dan kecepatan kompilasinya terasa hampir seketika
Lebih baik lagi tentu selalu bagus, tetapi kondisinya sudah cukup hebat
Sekarang, dengan cheat code bernama ChatGPT, saya bisa melewati hampir semua masalah Rust sulit yang beberapa tahun lalu mungkin membuat saya buntu, jadi prospek Rust tampak cukup bagus
Saat itu build image Docker memakan 60–90 menit, dan saya benar-benar merasakan betapa banyaknya dependensi seluruh proyek
Apakah ada cara mematikan opsi compiler paralel tanpa membangun ulang compiler?
Saya tidak perlu memakainya, lagi pula codegen units juga sudah saya setel ke 1, dan sepertinya ini memicu ICE yang tidak ingin saya debug
Saya tahu default-nya 1 thread, tetapi saya ingin mematikannya sepenuhnya
“Mode multithread punya bug yang diketahui, termasuk deadlock. Jika kompilasi berhenti, kemungkinan besar Anda terkena salah satunya.”
Kalau begitu saya akan menunggu sedikit lebih lama sebelum memakai
-Z threads;)