2 poin oleh GN⁺ 2023-11-11 | 1 komentar | Bagikan ke WhatsApp
  • 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

 
GN⁺ 2023-11-11
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

    • Sepertinya kecil kemungkinan akan membaik secara dramatis
      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
    • rust-analyzer memakan resource bahkan lebih banyak daripada rustc sendiri
      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 cukup sulit setuju dengan ini
      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 generic
      Akan menarik kalau Anda mencoba membangun proyek ini dengan cargo build dan memberi masukan soal waktu kompilasinya
      [1]: https://github.com/Orange-OpenSource/hurl
    • Sebagai orang yang baru pernah mengerjakan proyek kecil dengan Rust, saya penasaran kira-kira jumlah baris kode-nya seberapa banyak
      Dan juga apakah proyeknya dipecah menjadi beberapa crate pada titik yang tepat
    • Saya penasaran sebenarnya seberapa lambat
      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

    • Ada compiler Rust tanpa borrow checker bernama mrustc[0], jadi setidaknya sebagian versi Rust bisa dikompilasi tanpa borrow checker
      Namun mungkin saja itu tidak menghasilkan kode yang paling optimal
      Setahu saya ada optimisasi yang memakai informasi yang ditetapkan selama borrow checking, seperti optimisasi noalias yang 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
    • Secara ketat, menurut saya tidak harus begitu
  • Apakah ada cara untuk menggunakan jumlah core CPU, alih-alih menanam nilai tetap di file konfigurasi yang dipakai di mesin berbeda?

    • Perlu diingat ini masih tahap eksperimen dan terbatas untuk nightly, jadi belum disiapkan untuk penggunaan umum
      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
    • Anda bisa memakai variabel lingkungan RUSTFLAGS, dan itu juga disebutkan di artikel:

      $ RUSTFLAGS="-Z threads=8" cargo build --release

    • Karena memakai protokol jobserver dan Cargo secara default menginisialisasinya dengan jumlah core, sepertinya jika flag baru disetel ke nilai sangat tidak realistis seperti 10000, penggunaannya akan dibatasi oleh jumlah core yang tersisa
  • 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

    • Waktu kompilasi saya umumnya baik-baik saja, kecuali saat membuat image Docker lintas arsitektur di GitHub Actions
      Saat itu build image Docker memakan 60–90 menit, dan saya benar-benar merasakan betapa banyaknya dependensi seluruh proyek
    • Waktu kompilasi sangat banyak bergantung pada jumlah, ukuran, dan kompleksitas dependensi
  • 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 ;)