1 poin oleh GN⁺ 2025-06-28 | 1 komentar | Bagikan ke WhatsApp
  • Saat berulang kali membangun situs web yang dibuat dengan Rust menggunakan Docker, muncul masalah waktu build
  • Dengan konfigurasi Docker dasar, rebuild seluruh dependensi terjadi setiap kali dan memakan waktu lebih dari 4 menit
  • Meski menggunakan cargo-chef dan alat caching, build biner final masih tetap memakan banyak waktu
  • Hasil profiling menunjukkan bahwa sebagian besar waktu dihabiskan untuk LTO (link-time optimization) dan optimasi modul LLVM
  • Dengan menyesuaikan opsi optimasi, informasi debug, dan pengaturan LTO, ada sedikit peningkatan, tetapi kompilasi biner final tetap membutuhkan setidaknya 50 detik

Masalah dan latar belakang

  • Setiap kali mengubah situs pribadi yang dibuat dengan Rust, pekerjaan merepotkan berupa membangun biner static link, menyalinnya ke server, lalu me-restart layanan terus berulang
  • Ingin beralih ke deployment berbasis kontainer seperti Docker atau Kubernetes, tetapi kecepatan build Docker untuk Rust ternyata menjadi masalah besar
  • Di dalam Docker, bahkan perubahan kode kecil pun membuat seluruh proyek harus dibangun ulang dari awal, sehingga sangat tidak efisien

Build Rust di Docker – pendekatan dasar

  • Pendekatan Dockerfile yang umum adalah menyalin semua dependensi dan source code lalu menjalankan cargo build
  • Dalam kasus ini, tidak ada keuntungan caching sehingga full rebuild terus berulang
  • Untuk situs web penulis, full build memakan waktu sekitar 4 menit—belum termasuk waktu tambahan untuk mengunduh dependensi

Meningkatkan caching build Docker – cargo-chef

  • Dengan alat cargo-chef, hanya dependensi yang bisa lebih dulu di-cache dalam layer terpisah
  • Dengan begitu, saat kode berubah, build dependensi bisa digunakan kembali sehingga kecepatan build diharapkan meningkat
  • Dalam penerapan nyata, hanya 25% dari total waktu yang terpakai untuk build dependensi, dan build biner layanan web final masih tetap memakan waktu cukup besar (2 menit 50 detik~3 menit)
  • Meskipun hanya terdiri dari dependensi utama (axum, reqwest, tokio-postgres, dll.) dan sekitar 7.000 baris kode sendiri, satu eksekusi rustc tetap membutuhkan 3 menit

Analisis waktu build rustc: cargo --timings

  • Dengan cargo --timings, kita bisa melihat waktu build untuk tiap crate (unit kompilasi)
  • Hasilnya menunjukkan bahwa build biner final memakan sebagian besar total waktu
  • Ini membantu untuk analisis yang lebih rinci, tetapi masih kurang untuk memahami perilaku internal kompiler secara spesifik

Memanfaatkan profiling bawaan rustc (-Zself-profile)

  • Mengaktifkan fitur profiling bawaan rustc dengan flag -Zself-profile untuk mengukur waktu tiap operasi secara detail
  • Untuk itu, profiling diaktifkan melalui variabel lingkungan
  • Saat dianalisis dengan alat ringkasan (summarize), ditemukan bahwa LLVM LTO (link-time optimization) dan code generation modul LLVM menyumbang lebih dari 60% dari total waktu
  • Visualisasi flamegraph juga menunjukkan bahwa tahap codegen_module_perform_lto menghabiskan 80% dari total waktu

LTO (link-time optimization) dan opsi optimasi build

  • Build Rust pada dasarnya dibagi menurut codegen unit, lalu optimasi menyeluruh diterapkan relatif di tahap akhir melalui LTO
  • LTO memiliki beberapa opsi seperti off, thin, fat: masing-masing memengaruhi performa dan hasil akhir
  • Pada proyek penulis, Cargo.toml mengatur LTO ke thin dan simbol debug ke full
  • Dari pengujian berbagai kombinasi LTO/simbol debug:
    • Dikonfirmasi bahwa simbol debug full meningkatkan waktu build, dan fat LTO menyebabkan build melambat sekitar 4 kali lipat
    • Bahkan setelah menghapus LTO dan simbol debug, build masih membutuhkan setidaknya 50 detik

Optimasi tambahan dan catatan

  • Sekitar 50 detik sebenarnya bukan masalah besar untuk situs penulis yang hampir tidak memiliki beban layanan nyata, tetapi rasa penasaran teknis mendorong analisis lebih lanjut
  • Kompilasi inkremental (incremental compilation) juga bisa mempercepat build jika dimanfaatkan dengan baik di Docker, tetapi perlu dipadukan dengan kebersihan environment build dan cache Docker

Profiling detail tahap LLVM

  • Bahkan setelah menghapus LTO dan simbol debug, tahap LLVM_module_optimize masih menghabiskan hampir 70% waktu
  • Disadari bahwa biaya optimasi besar berasal dari nilai default opt-level (3) pada profil release, lalu diuji metode menurunkan opt-level hanya untuk biner
  • Dari berbagai eksperimen kombinasi optimasi, tanpa optimasi (opt-level=0) waktu build sekitar 15 detik, sedangkan dengan optimasi (1~3) sekitar 50 detik

Analisis mendalam event trace LLVM

  • Dengan flag tambahan rustc (-Z time-llvm-passes, -Z llvm-time-trace), waktu eksekusi tiap tahap LLVM bisa ditelusuri secara detail
  • -Z time-llvm-passes menghasilkan output yang sangat besar sehingga sering melampaui batas log Docker, jadi pengaturan log perlu disesuaikan
  • Jika log disimpan ke file untuk dianalisis, waktu eksekusi tiap LLVM optimization pass dapat dilihat satu per satu
  • Opsi -Z llvm-time-trace menghasilkan output JSON besar dalam format chrome tracing; ukurannya sangat besar sehingga sulit dianalisis dengan editor teks atau alat analisis biasa
  • Dengan memecahnya per baris (jsonl), data itu dapat dianalisis di lingkungan CLI atau skrip

Insight utama dan kesimpulan

  • Saat membangun proyek Rust yang kompleks di Docker, bottleneck kecepatan build terutama terjadi pada build biner final dan tahap optimasi LLVM terkait
  • Saat menyesuaikan LTO, simbol debug, dan opt-level, terdapat trade-off yang jelas antara waktu build dan ukuran biner
  • Dengan menyesuaikan opsi optimasi secara agresif, waktu build bisa dipangkas drastis, tetapi performa juga bisa menurun jika optimasi dimatikan
  • Jika efisiensi build penting dalam lingkungan produksi dan proyek dengan dependensi crate besar, strategi yang baik adalah memanfaatkan profiling secara aktif untuk mengidentifikasi bottleneck detail secara konkret
  • Saat merancang pipeline build Rust, diperlukan kombinasi yang matang antara LTO, opt-level, simbol debug, dan strategi cache

1 komentar

 
GN⁺ 2025-06-28
Opini Hacker News
  • Menarik bahwa proyek Rust sering tampak kecil di permukaan. Pertama, dependensi tidak berkaitan langsung dengan ukuran nyata codebase. Di C++, dependensi proyek besar sering di-vendor atau bahkan tidak dipakai sama sekali, jadi kalau ada 400 ribu baris kode yang lambat, orang bisa berpikir, "ya wajar lambat kalau kodenya sebanyak itu." Kedua, bagian yang jauh lebih bermasalah adalah macro. Macro yang berulang kali mengembang 10 atau 100 baris bisa dengan cepat mengubah proyek 10 ribu baris menjadi sejuta baris. Ketiga adalah generic. Setiap instansiasi generic menghabiskan resource CPU. Meski begitu, sedikit membela: fitur-fitur ini juga memberi keuntungan, misalnya sesuatu yang butuh 100 ribu baris di C, atau 25 ribu baris di C++, bisa menyusut jadi hanya beberapa ribu baris di Rust. Namun memang benar, saat fitur-fitur ini dipakai berlebihan, ekosistem jadi tampak lambat. Misalnya di perusahaan kami memakai async-graphql; library-nya sendiri sangat bagus, tetapi sangat bergantung pada procedural macro. Isu terkait performa sudah terbuka selama bertahun-tahun, dan setiap kali menambah data type, compiler terasa jelas makin lambat

    • Saya penasaran kenapa sering ada kasus menulis ulang utilitas C kecil ke Rust, padahal kode asalnya memang sederhana. Dibanding porting program C besar 100 ribu baris ke Rust, yang lebih sering terlihat justru kode berukuran sangat kecil. Saya penasaran bagaimana perbandingan kecepatan kompilasi Rust dan C untuk program kecil. Yang saya maksud bukan ukuran program, tetapi kecepatan kompilasinya. Sebagai referensi, pengukuran terbaru menunjukkan ukuran toolchain compiler Rust sekitar 2 kali GCC yang saya pakai. 1. Untuk program kecil seperti ini, kemungkinan ada isu memory safety yang tersembunyi biasanya rendah, dan karena skalanya kecil, audit juga mudah. Situasinya berbeda dengan program C 100 ribu baris
    • Setiap kali mendefinisikan type baru, compiler terasa melambat. Seingat saya, performa compiler melambat secara eksponensial tergantung pada "kedalaman" type. Dalam kasus seperti GraphQL, yang punya banyak nested type, masalah ini terasa sangat parah
    • Untuk menangani masalah macro yang mengembang puluhan atau ratusan baris sehingga codebase bisa membesar secara geometris, baru-baru ini ditambahkan dukungan alat analisis. Lihat referensi terkait: https://nnethercote.github.io/2025/06/26/how-much-code-does-that-proc-macro-generate.html
  • Ryan Fleury membuat Epic RAD Debugger dalam C, 278 ribu baris, dengan gaya unity build (semua kode menjadi satu file sebagai satu unit kompilasi), dan clean compile di Windows cuma butuh 1,5 detik. Dari contoh ini saja sudah terlihat bahwa kompilasi bisa sangat cepat, jadi saya penasaran kenapa hal serupa tidak bisa dicapai di Rust atau Swift

    • Semakin banyak hal yang dilakukan compiler saat build time, semakin lama waktu build. Go bisa mencapai build time di bawah 1 detik bahkan untuk codebase besar. Ia punya sistem modul dan type system sederhana yang meminimalkan pekerjaan saat build, dan sebagian besar fitur diserahkan ke runtime GC. Sebaliknya, jika membutuhkan macro, type system kompleks, dan tingkat robustness tinggi, build time mau tidak mau akan lebih panjang
    • Di Rust juga unit build-nya adalah satu crate penuh, dan compiler memecahnya ke LLVM IR menjadi ukuran yang sesuai. Ia juga menyeimbangkan pekerjaan duplikat dan incremental build dengan sendirinya. Dalam banyak kasus, Rust lebih cepat build dibanding C++ jika dihitung berdasarkan baris source code. Namun, proyek Rust punya karakteristik bahwa semua dependensi juga ikut dikompilasi
    • Alasan Rust dan Swift lebih lambat dikompilasi daripada compiler C adalah karena bahasanya sendiri memerlukan jauh lebih banyak analisis. Misalnya, borrow checker Rust tidak datang secara gratis. Pengecekan saat compile time saja sudah menghabiskan resource yang besar. C cepat karena nyaris tidak memeriksa apa pun di luar sintaks dasar. Bahkan kombinasi aneh seperti memanggil foo(int) ke foo(char*) pun tidak dicek
    • Saya pernah mengompilasi proyek C++ puluhan ribu baris pada era 2000-an, dan bahkan di komputer tua pun build selesai dalam kurang dari 1 detik. Sebaliknya, HELLO WORLD yang hanya memakai Boost butuh beberapa detik. Pada akhirnya, kecepatan build sangat dipengaruhi bukan hanya oleh bahasa atau compiler, tetapi juga oleh struktur kode dan fitur yang digunakan. Anda bisa saja membuat DOOM dengan macro C, tetapi kemungkinan besar tidak akan cepat. Sebaliknya, Rust juga bisa distrukturkan agar build-nya cepat
    • Tidak terlalu mengejutkan kalau bahasa seperti C dan Go, yang memang mengejar kompilasi cepat, benar-benar cepat. Yang betul-betul sulit adalah mengompilasi semantik Rust dengan cepat. Masalah ini juga ada di FAQ resmi Rust
  • Saya sangat bersyukur Go memprioritaskan kecepatan kompilasi dibanding optimisasi. Untuk server, networking, dan pekerjaan glue code, kompilasi yang sangat cepat adalah hal paling penting. Saya juga ingin type safety secukupnya, tapi tidak sampai menghalangi prototyping yang longgar. Keberadaan GC juga praktis. Menurut saya, setelah pengalaman mengembangkan dalam skala besar di Google, mereka sampai pada kesimpulan bahwa type yang sederhana, GC, dan kompilasi super cepat jauh lebih penting daripada kecepatan eksekusi atau kesempurnaan semantik. Melihat contoh software networking dan infrastruktur skala besar yang dibuat dengan Go, pilihan itu terasa sangat tepat. Tentu saja, Go mungkin tidak dipakai di lingkungan yang tidak bisa menerima GC atau yang lebih mementingkan ketepatan absolut, tetapi untuk lingkungan kerja saya, pilihan Go adalah yang paling optimal

    • Saya juga suka Go, tetapi saya tidak menganggap bahasa ini sebagai hasil kecerdasan kolektif luar biasa dari organisasi besar seperti Google. Kalau pengalaman Google benar-benar banyak terserap, misalnya mereka pasti sudah menambahkan fitur seperti eliminasi statis null pointer exception. Malah rasanya lebih seperti beberapa developer Google membuat bahasa yang memang mereka inginkan
    • Go memang punya kelebihan seperti kompilasi cepat, type system secukupnya, dan GC, tetapi dalam ruang desain, Java sebenarnya sudah menempati posisi yang mirip. Rasanya Go lahir lebih karena dorongan kreatif, dan pada akhirnya justru lebih banyak diserap oleh pengguna bahasa scripting (Python/Ruby/JS) daripada target aslinya (server-side C/C++/Java). Pengguna scripting hanya menginginkan type system yang mudah dan cepat, sementara Java terasa terlalu tua dan tidak menyenangkan. Java sendiri sudah tidak punya ruang lagi di ranah server/konferensi/library
    • Ada juga cerita bahwa developer Google merancang Go sambil menunggu proyek C++ dikompilasi
    • Saya ingin bertanya apa itu "obnoxious type". Type itu hanya bisa merepresentasikan data dengan benar atau tidak, dan pada praktiknya di bahasa mana pun type checker selalu bisa dipaksa diam
    • Go adalah bahasa yang sangat cocok dengan tujuan desain dan penggunaan nyatanya. Risiko terbesarnya adalah pemrosesan paralel dan cara berbagi mutable state melalui channel, karena di bagian ini bisa muncul bug yang halus atau rapuh. Biasanya sebagian besar pengguna tidak memakai pola seperti itu. Saya sendiri memakai Rust, dan pekerjaan saya menuntut algoritma yang memang lambat diperas semaksimal mungkin di hardware yang lambat. Akibatnya, paralelisasi skala besar hampir mustahil dilakukan karena masalahnya sangat subtil
  • Saya tidak paham argumen bahwa instalasi binary statis tunggal lebih sederhana daripada pengelolaan container

    • Sepertinya penulis belum benar-benar memahami apa yang sebenarnya dilakukan docker. Misalnya ada pernyataan, "kalau deploy dengan image docker berarti setiap kali harus build semuanya dari nol," padahal di lingkungan build/deploy internal hal seperti itu tidak harus terjadi. Untuk penggunaan pribadi pun, tidak masalah hanya memasukkan file hasil build lokal ke dalam container selama kenyamanan development tetap terjaga. Yang perlu diperhatikan hanya path jejak lingkungan build. Dalam CI/CD atau proyek tim, fokusnya memang menjamin build dapat dibuat dari nol di mana saja, tetapi untuk pekerjaan pribadi itu tidak wajib
    • Di tulisan aslinya, tujuannya bukan penyederhanaan melainkan modernisasi. Maksudnya semacam, "karena dalam 10 tahun terakhir sebagian besar software menjadikan deployment container sebagai standar, maka situs web saya juga akan dideploy sebagai container seperti docker atau kubernetes." Container punya banyak keuntungan seperti isolasi proses, keamanan, logging terstandar, dan skalabilitas horizontal
  • Di laptop saya (Mac M4 Pro), clean compile penuh Deno (proyek Rust besar) butuh 2 menit. Kalau dilihat dari command, debug sekitar 1 menit 54 detik, release sekitar 8 menit 17 detik. Angka ini diukur tanpa incremental compilation. Sebenarnya build untuk deployment dijalankan di sistem CI/CD, jadi saya sendiri tidak perlu menunggu langsung

    • Ada artikel terkait yang menyebut sekitar 6 menit di M1 Max dan 11 menit di M1 Air
  • Bagian tentang Cranelift ada di mana? Menurut saya, saya hampir menyerah mengembangkan game dengan Rust karena waktu kompilasinya terlalu lama. Setelah saya telusuri, ternyata LLVM lambat terlepas dari level optimisasi. Ini juga selalu disorot oleh pengembang bahasa Jai. Saya pernah mengalami build time turun dari 16 detik menjadi 4 detik dengan Cranelift. Salut untuk tim Cranelift!

    • Baru-baru ini di Bevy game jam, kami memakai tool bernama 'subsecond' dari komunitas Dioxus, dan sesuai namanya, tool itu memungkinkan system hot reload di bawah 1 detik, sangat membantu untuk prototyping UI. https://github.com/TheBevyFlock/bevy_simple_subsecond_system
    • Setahu saya tim zig juga berusaha membuat compiler (backend) sendiri tanpa LLVM untuk mempercepat build time secara signifikan
    • Dulu saya kira Cranelift belum mendukung macOS aarch64, ternyata belakangan saya tahu sekarang sudah didukung
    • Agak berlebihan juga kalau bilang hampir menyerah pada Rust hanya karena build time 16 detik, bukan?
  • Saya tidak menganggap Rust itu lambat. Dibanding bahasa sekelasnya, ia cukup cepat, dan dibanding kompilasi C++/Scala yang bisa makan 15 menit, Rust jauh lebih cepat

    • Saya juga setuju. Saya pribadi tidak pernah merasa build Rust itu mengganggu. Mungkin citra buruk dari masa awal terus terbawa sehingga penilaian seperti ini tetap ada
    • Penggunaan memori saat kompilasi jauh lebih besar dibanding C/C++. Kalau saya ingin mengompilasi proyek Rust besar di VM untuk demo YouTube, saya butuh lebih dari 8GB. Di C/C++, saya tidak pernah perlu mengkhawatirkan hal seperti itu
    • Karena template C++ itu Turing-complete, membandingkan build time tanpa mempertimbangkan gaya kode nyata sebenarnya tidak terlalu bermakna
  • Dari sudut pandang mantan developer C++, saya kurang paham klaim bahwa build Rust itu lambat

    • Itulah kenapa Rust sering dinilai menargetkan developer C++. Developer yang lama berkutat dengan C++ sudah punya semacam Stockholm syndrome terhadap ketidaknyamanan tool
    • Walaupun lebih cepat dari C++, bukan berarti secara absolut tidak lambat. Reputasi buruk build C++ memang sudah terkenal sangat parah. Rust tidak membawa masalah bahasa yang struktural seperti itu, jadi ekspektasinya jadi lebih tinggi
    • Rasanya ini contoh klasik di mana fitur baru terus ditambahkan, tetapi masukan pengguna nyata dan penyelesaian masalah tidak terlalu didengar
    • Tahap kompilasi C itu sedikit dan sederhana sehingga cepat, tetapi pada C++ saya merasa penggunaan template justru menghancurkan banyak encapsulation work. Mengubah satu template header saja rasanya bisa memengaruhi 98% seluruh proyek
  • Incremental compilation itu benar-benar sangat kuat. Setelah build awal, Anda bisa membekukan snapshot incremental cache dan jika tidak ada perubahan, itu bisa langsung dipakai untuk build/deploy cepat. Ini juga cocok dengan docker. Selain versi compiler atau pembaruan besar situs web, layer image build biasanya tidak perlu disentuh. Kalau hanya ada perubahan kode, cukup atur agar layer tersebut tidak ikut dibangun ulang sehingga efisien

    • Artifact incremental proyek saya sudah lebih dari 150GB. Saat saya pernah memakai image docker sebesar itu, benar-benar muncul masalah-masalah besar
  • Build time homepage saya 73ms. Static site generator melakukan kompilasi ulang hanya dalam 17ms. Eksekusi generator aslinya sendiri cuma 56ms. Saya lampirkan log build zig

    • Rasanya di C/C++ selalu ada komentar yang bilang Rust bagus, dan di Rust selalu ada komentar yang bilang Zig bagus. (Ternyata penulis komentar ini adalah pengembang utama zig.) Menurut saya penginjilan bahasa itu buruk bagi komunitas, dan pada praktiknya hanya menimbulkan antipati alih-alih mendatangkan pengguna baru. Kalau benar-benar mencintai sebuah bahasa, akan lebih membantu jika budaya penginjilan seperti ini ditekan
    • Daripada hanya memberi satu metrik waktu kompilasi, akan lebih bagus kalau ada pembahasan atau interpretasi yang langsung terkait dengan topik tulisan asli
    • Situs web Rust saya (termasuk framework mirip React dan web server yang benar-benar berjalan) juga butuh sekitar 1,25 detik saat incremental build dengan cargo watch. Kalau memakai hal seperti subsecond[0], termasuk incremental linking dan hotpatch, bisa lebih cepat lagi. Memang tidak secepat Zig, tetapi hampir mendekati. Kalau 331ms yang disebut di atas adalah clean build (tanpa cache), maka itu jauh lebih cepat daripada clean build situs saya yang 12 detik. [0]: https://news.ycombinator.com/item?id=44369642
    • Saya sangat ingin bertanya kepada @AndyKelley, menurutnya apa alasan penentu mengapa zig bisa mengompilasi sangat cepat sementara Rust dan Swift selalu lambat
    • Bukankah Zig memang tidak menjamin memory safety?