Perjalanan penulisan ulang compiler Roc dari Rust ke Zig
(rtfeldman.com)- Untuk mengatasi cacat struktural pada implementasi lama, 300 ribu baris Rust ditulis ulang ke Zig, dan kesetaraan fitur dicapai dalam 487 hari dengan target rilis bernomor resmi pertama, 0.1.0, pada paruh akhir tahun ini
- Kompiler baru mendukung hot code loading dan cross-compilation yang dapat direproduksi, interpolasi string di dalam pattern matching, serta penghapusan alokasi heap pada HTTP routing, dan juga memangkas ukuran wasm
Rocci Birdmenjadi 31KB, kurang dari setengahnya - Alasan utama memilih Zig adalah waktu build, kontrol allocator dan tata letak data yang lebih terperinci, ekosistem yang cocok untuk pengembangan kompiler, serta pemeriksaan kode yang tidak aman terhadap memori; incremental build di Zig 0.17.0 membangun ulang sekitar 460 ribu baris dalam 35ms
- Dalam klasifikasi bug nyata, kompiler Rust mencatat 21 kasus kerusakan memori dan kompiler Zig 10 kasus, tetapi sebagian besar disebabkan oleh pembuatan kode yang salah; kesalahan keamanan memori pada kompiler Zig sendiri hanya 2 kasus use-after-free yang merusak nama file
- Zig cocok untuk struktur data tanpa pointer, deserialisasi tanpa parsing, dan penggunaan ulang serializer LLVM bitcode, tetapi dalam pembebasan memori otomatis pada pengujian, polimorfisme, field struct privat, deteksi dead code, dan kompatibilitas antarrilis, pengalaman pengembangan di Rust dinilai lebih baik
Penulisan ulang yang mencapai kesetaraan fitur
- Tim kompiler Roc menulis ulang sekitar 300 ribu baris Rust ke Zig selama kurang lebih satu setengah tahun dan mencapai kesetaraan fitur dengan kompiler lama
- Kesetaraan fitur adalah tonggak penting, tetapi bukan rilis resmi, dan 0.1.0 untuk kompiler baru ditargetkan pada paruh akhir tahun ini
- Game WASM-4 2024 Rocci Bird kini bisa dibangun dengan kompiler baru
- Seluruh game terdiri dari kurang dari 1.000 baris kode Roc
- Sumber yang diperbarui lebih ringkas dibanding versi asli
- Biner wasm yang dihasilkan
roc build --opt=sizeberukuran 31KB, lebih kecil dari setengah hasil kompiler lama
- Di halaman utama, program Roc dasar bisa ditulis dan dijalankan di browser dengan kompiler WebAssembly 2,5MB
- Penulisan ulang ini memakan waktu 487 hari, 476 hari lebih lama daripada 11 hari yang dibutuhkan Bun untuk memindahkan sekitar 500 ribu baris Zig ke Rust
- Bun melakukan porting langsung, sedangkan Roc lebih mendekati penulisan ulang yang juga banyak mengubah struktur dan fitur kompiler
- Karena itu, sulit membandingkan skala atau durasi kedua codebase tersebut secara langsung hanya berdasarkan Rust dan Zig
Pengalaman pengembangan yang ditawarkan kompiler baru
-
Hot code loading dan cross-compilation
- Setelah menjalankan server dengan
roc server.roc, jika kode diubah maka kode baru otomatis diterapkan mulai permintaan berikutnya - Ini umum pada bahasa interpreter seperti Python, tetapi jarang pada bahasa kompilasi berperforma tinggi seperti Roc
- Mekanisme yang sama bekerja bukan hanya untuk web server, tetapi juga game 2D sederhana
- Saat didistribusikan,
roc build server.rocmenghasilkan biner standalone dengan optimasi LLVM diterapkan - Dengan
roc build --target=x64musl, biner statis untuk Alpine Linux bisa di-cross-compile - Jika byte sumber yang dimasukkan sama, maka byte keluaran yang sama dihasilkan di sistem build mana pun, termasuk Mac
- Setelah menjalankan server dengan
-
Interpolasi string di dalam pattern matching
- Interpolasi string seperti
"/users/${id}"didukung di dalam pattern matching - Tidak seperti routing ala Express yang mem-parsing template string saat runtime, kompiler menanganinya secara langsung
- Method HTTP dan path bisa dicocokkan sekaligus, dan path bertingkat maupun nilai default juga bisa dibedakan dengan pattern
- Seluruh kode pemrosesan HTTP request pada contoh tidak melakukan alokasi heap sama sekali sambil tetap memeriksa keamanan tipe saat kompilasi
- Routing HTTP tanpa alokasi diimplementasikan dengan memanfaatkan eksekusi saat kompilasi dari fungsi murni, dan sintaksnya bisa dicoba di kompiler WebAssembly di halaman utama
- Interpolasi string seperti
Alasan menulis ulang dari awal
- Roc, tidak seperti Rust, C, dan Zig, bukan bahasa sistem dan menggunakan manajemen memori otomatis berbasis reference counting
- Ini menghindari jeda dari tracing garbage collector
- Roc memanfaatkan optimisasi Perceus dan mutasi oportunistik ala Koka
- Tidak seperti bahasa non-sistem pada umumnya yang mengalokasikan heap untuk setiap closure capture, Roc menggunakan defunctionalization polimorfik melalui spesialisasi lambda set
- Defunctionalization memungkinkan banyak optimasi lanjutan seperti inlining pada bahasa fungsional, tetapi membuat implementasi lama benar-benar akurat terbukti sangat sulit
- Melalui prototipe OCaml Ayaz Hafiz, tim memastikan bahwa masalahnya adalah cacat struktural yang melintasi banyak tahap kompiler, dan untuk menyelesaikannya sebagian besar kompiler harus ditulis ulang
- Kontributor lain juga berencana menulis ulang berbagai bagian karena alasan masing-masing, sehingga alih-alih penggantian bertahap ala kapal Theseus yang mengganti hampir semuanya sedikit demi sedikit, penulisan ulang total mulai dipertimbangkan
- Dalam proyek kompiler yang sukses, penulisan ulang dari awal sering terjadi, misalnya untuk self-hosting, dan ada juga kasus penulisan ulang TypeScript yang dipindahkan ke Go, bukan ke bahasanya sendiri
- Meski kebijakan Roc adalah tidak melakukan self-hosting pada kompiler, kali ini manfaat yang diperoleh dari penulisan ulang dinilai lebih besar daripada biaya yang sudah diketahui
Kriteria memilih Zig alih-alih kembali ke Rust
- Tim sudah menggunakan Zig untuk berbagai fungsi dasar di standard library, dan karena bahasa sistem yang benar-benar mereka kuasai hanya Rust dan Zig, hanya dua bahasa itu yang dipertimbangkan secara serius
- Bahasa yang cocok berbeda untuk setiap proyek, dan tetap memakai Rust untuk pekerjaan lain tidak bertentangan dengan memilih Zig untuk Roc
-
Waktu build
- Build Rust dengan
cargomemakan waktu lama bahkan untuk incremental build, dan makin menjadi hambatan utama seiring membesarnya codebase - Tim memperkirakan build akan jauh lebih cepat setelah pindah ke Zig
- Build Rust dengan
-
Kontrol memori dan tata letak data
- Di setiap tahap kompilasi digunakan berbagai allocator, terutama arena allocator, serta tata letak struct-of-arrays (SoA) secara luas
- Ekosistem Rust pada umumnya mengasumsikan satu allocator global, sedangkan ekosistem Zig menjadikan penerusan allocator yang terperinci sebagai dasar dan juga menyediakan dukungan SoA secara standar
-
Cakupan ekosistem yang dibutuhkan
- Ekosistem Rust secara keseluruhan memang lebih besar daripada Zig, tetapi paket yang berkaitan dengan kebutuhan khusus Roc tidak banyak di kedua ekosistem
- Fitur khusus yang dibutuhkan, seperti pembuatan bitcode cepat alih-alih membungkus library LLVM C++, lebih banyak tersedia di sisi Zig
-
Dukungan untuk kode yang tidak aman terhadap memori
- Kompiler Rust lama dengan 300 ribu baris memiliki sekitar 1.200 penggunaan
unsafe - Sebagai pembanding, kompiler Rust sendiri memiliki sekitar 40 ribu
unsafedalam 3,5 juta baris - Dalam kompiler yang menghasilkan kode mesin, operasi yang tidak aman terhadap memori adalah bagian penting dari pekerjaan
- Model Rust adalah mengisolasi blok
unsafeyang jarang dan memeriksanya dengan Miri atau Valgrind, tetapi di Rocunsafebukan sesuatu yang jarang - Zig menyediakan lebih banyak fitur untuk membuat kode yang tidak aman terhadap memori bekerja dengan benar, dan justru di area inilah tim paling menginginkan dukungan
- Kompiler Rust lama dengan 300 ribu baris memiliki sekitar 1.200 penggunaan
Keamanan memori yang diperoleh tanpa borrow checker
-
Cakupan pemeriksaan Rust dan Zig
- Menurut data Microsoft tahun 2019, sekitar 70% kerentanan yang ditangani melalui pembaruan keamanan setiap tahun adalah masalah keamanan memori
- Jika klasifikasi tahun 2018 pada data tersebut dibagi dari sudut pandang Rust dan Zig, hasilnya sebagai berikut
- 83,6% mencakup baca/tulis di luar batas, cast yang tidak aman, pembacaan tanpa inisialisasi, stack overflow, dan masalah non-keamanan memori, yaitu kategori yang tidak dipengaruhi oleh pilihan Rust atau Zig
- 16,4% adalah use-after-free, yaitu kategori yang bisa ditangkap oleh borrow checker Rust, Zig
ReleaseSafe, dan pemeriksaan gaya Fil-C ReleaseSafemilik Zig akan memicu panic saat runtime jika memori yang sudah dibebaskan digunakan- Pemeriksaannya kurang menyeluruh dibanding subset aman Rust
- Ada biaya runtime dan program bisa panic
ReleaseFastmenghilangkan pemeriksaan di produksi, tetapi tetap mempertahankannya pada debug build dan pengujian- Jika semua jalur eksekusi nyata diuji, keamanan setara
ReleaseSafebisa dicapai, tetapi cakupan pengujian seperti itu umumnya tidak realistis
-
Proyek Zig lain dan
unsafedi Rust- TigerBeetle menggunakan
ReleaseSafe, dan dalam verifikasi Jepsen yang sangat teliti ditemukan 2 bug keamanan, tetapi keduanya tidak terkait keamanan memori - Ghostty dan compiler Zig yang memakai
ReleaseFastjuga tidak memiliki CVE yang disebabkan oleh ketidakamanan memori pada kode Zig - Program Rust juga bisa memiliki celah keamanan memori melalui
unsafedi dalam dependensi - Unsafe Rust memiliki risiko yang mirip dengan Zig
ReleaseFast, tetapi tidak ada fitur yang setara dengan pemeriksaan runtime Zig untuk menangkap masalah selama pengembangan - Miri dan Valgrind dapat membantu, tetapi tidak banyak proyek Rust yang menggunakannya
- Sebagai gantinya, budaya penggunaan
unsafeyang jarang dan peninjauan yang lebih ketat membentuk reputasi kuat Rust dalam keamanan memori di praktik nyata - Bahkan pada proyek berbasis Rust, pernah ada kasus kerentanan terkait
unsafe- Deno pernah memiliki CVE out-of-bounds read dan use-after-free
- Rocket pernah memiliki CVE use-after-free
- Actix mengalami beberapa CVE ketidakamanan memori saat penggunaan
unsafesangat tinggi secara tidak normal
- Roc menilai use-after-free bukan risiko besar karena sebagian besar alokasi terjadi di arena dengan umur yang sederhana, dan menilai pemeriksaan tambahan Zig lebih berguna untuk kode yang secara inheren tidak aman
- TigerBeetle menggunakan
Bug korupsi memori yang terkonfirmasi setelah penulisan ulang
- Hasil klasifikasi issue tracker Roc dengan Claude Opus 4.8 adalah sebagai berikut
| Compiler | Terjadi korupsi memori | Tidak ada korupsi memori | Total |
|---|---|---|---|
| Rust | 21 | 2.575 | 2.596 |
| Zig | 10 | 421 | 431 |
- Ke-21 kasus korupsi memori pada compiler Rust bukanlah korupsi dalam logika internal compiler
- borrow checker menjalankan peran yang dimaksudkan
- Itu adalah bug miscompile di mana kode mesin yang dihasilkan salah lalu merusak memori pada program yang dikompilasi
- Dari 10 kasus korupsi memori pada compiler Zig, 8 di antaranya juga merupakan miscompile
- 2 sisanya adalah use-after-free pada kode pelaporan error
- Pada pesan error
roc checkdanroc bundle, nama file menjadi rusak sebagai karakter pengganti U+FFFD - Jika memakai borrow checker Rust, kedua bug itu bisa tertangkap
- Pada pesan error
- Dampak nyata pada pengguna dari pilihan alat yang berbeda adalah sebagai berikut
| Pilihan alat | Dampak nyata pada keamanan memori |
|---|---|
Zig ReleaseFast |
2 bug di mana nama file tidak dirender pada sebagian pesan error |
Zig ReleaseSafe |
2 bug di mana sebagian error panic dan nama file gagal dirender |
| borrow checker Rust | Kedua bug dicegah |
- Jika mempertimbangkan 18 bulan, ratusan ribu baris kode, dan ratusan laporan bug, perbedaan ketiga pilihan itu secara praktis tidak besar bagi proyek ini
- Bun, karena menangani nilai JavaScript dengan garbage collection terlacak bersama nilai yang dikelola manual, memiliki porsi besar masalah use-after-free, double-free, dan pembebasan yang terlewat
- Compiler Roc tidak berintegrasi dengan JavaScript atau garbage collector terlacak lainnya, sehingga tidak mengalami masalah manajemen umur yang sama
- Roc lebih membutuhkan alat untuk menemukan error memori pada kode output yang dihasilkan daripada error memori internal compiler, dan yang terakhir berada di luar cakupan borrow checker
Hasil nyata waktu build
zig build --watch -fincrementalsaat ini membangun ulang perubahan dalam sekitar 35ms pada sekitar 450 ribu hingga 460 ribu baris kode Zig- Zig 0.16.0 versi stabil memiliki bug yang merusak
-fincrementalpada codebase Roc- Perbaikannya sudah masuk, tetapi untuk memakainya harus pindah ke prabuild Zig 0.17.0 yang memutus kompatibilitas
- Karena dependensi terkait juga harus ikut di-vendor dan di-upgrade ke 0.17.0, diputuskan untuk menunggu rilis stabil berikutnya
- Hasil pengukuran pada desktop Intel dan Ubuntu 26 adalah sebagai berikut
| Compiler Roc | Ukuran kode | Cold build | Incremental build |
|---|---|---|---|
| Asli berbasis Rust 1.85.0 | 354K | 32,4 detik | 10,0 detik |
| Asli berbasis Rust 1.97.0 | 354K | 25,4 detik | 3,4 detik |
| Zig 0.16.0 saat mencapai kesetaraan fitur | 320K | 39,6 detik | 8,6 detik |
| Hasil penulisan ulang saat ini dengan Zig 0.17.0 | 464K | 32,1 detik | 0,035 detik |
- Saat mencapai kesetaraan fitur, artefak yang hampir tidak pernah berubah masih dibangun ulang setiap kali, tetapi sekarang hanya dihasilkan saat diperlukan
- Dengan perubahan ini, meski kode bertambah sekitar 50%, cold build justru menjadi lebih cepat
- Dari Rust 1.85.0 ke 1.97.0, incremental build turun dari 10 detik menjadi 3,4 detik, yaitu peningkatan sekitar dua pertiga selama 18 bulan
- Angka 35ms di Zig adalah sekitar seperseratus dari 3,4 detik, dan itu didapat dari codebase yang sekitar 50% lebih besar dibanding kode Rust yang diukur
- Saat ini
-fincrementalhanya berfungsi pada x86-64, dan karena banyak kontributor memakai Mac berbasis ARM, keunggulan waktu build itu masih belum bisa dimanfaatkan sepenuhnya
Struktur tanpa pointer dan deserialisasi tanpa parsing
- Cache disk baru menggunakan tata letak memori yang efisien saat dijalankan langsung sebagai format disk, seperti pendekatan yang dipakai compiler Zig dan pengembangan game
- Semua struktur data compiler direpresentasikan sebagai array indeks 32-bit alih-alih pointer, dan di banyak tempat menggunakan format array of structs
- Mengurangi penggunaan memori dan meningkatkan kecepatan eksekusi
- Struktur data bisa ditulis langsung ke disk tanpa diubah ke format serialisasi terpisah
- Saat deserialisasi, byte dari disk tidak diparsing
- Byte dibaca ke memori
- Sebagian data ditata ulang agar struktur data yang ada menunjuk ke array baru
- Setelah itu bisa langsung digunakan
- Kecepatannya pada dasarnya dibatasi oleh kecepatan I/O untuk membaca byte dari disk ke memori
- Jika data ada di cache disk sistem operasi, hasil build sebelumnya dimuat kira-kira dengan kecepatan
memcpy
- Jika data ada di cache disk sistem operasi, hasil build sebelumnya dimuat kira-kira dengan kecepatan
roc checkmenyimpan hasil parsing, type checking, dan lain-lain ke disk pada eksekusi pertama- Jika source input tidak berubah, pada eksekusi kedua struktur data langsung dipindahkan dari disk ke memori
roc testjuga melakukan cache untuk hasil pengujian fungsi murni yang deterministik- Cache bekerja per file, jadi jika satu file diubah hanya file itu dan file dependensinya yang diproses ulang
- Pendekatan ini dimungkinkan karena seluruh compiler menggunakan indeks alih-alih pointer, dan pada struktur umum yang berpusat pada pointer, deserialisasi tanpa parsing tidak mungkin dilakukan
-
Batasan keamanan struktur berbasis indeks
- Seperti pointer bisa menunjuk ke alamat yang salah, indeks juga bisa mengakses array yang salah dan membaca byte sembarang
- Rust borrow checker menangani masa hidup pointer, tetapi tidak memeriksa indeks mana milik array yang mana
- Jika jumlah array yang dibutuhkan diketahui sebelumnya,
compact_arenadi Rust bisa membuat tag tipe lewat makro untuk mencegah akses ke array yang salah - Jika seperti Roc jumlah array berubah sesuai jumlah modul, teknik ini tidak bisa diterapkan sehingga
compact_arenajuga menandaiSmallArena::newsebagaiunsafe - Pembuatan arena kosong sendiri tidak menimbulkan bahaya, tetapi risiko sebenarnya ada pada indexing array yang dilakukan sangat sering
- Safe Rust efektif dengan asumsi bahwa
unsafekecil dan terisolasi, tetapi pada Roc asumsi ini tidak berlaku karenaunsafetersebar luas
Ekosistem Zig yang cocok untuk Roc
- Bun merasa
Dropdi Rust yang menjalankan kode cleanup hanya sekali itu berguna karena interoperabilitas antara JavaScript dan memori manual - Sebaliknya, Roc ingin memakai arena terpisah untuk tiap modul dan tahap kompilasi, sehingga package Rust yang mengandaikan allocator global dan
Dropimplisit terasa kurang nyaman - Di ekosistem Zig, API yang secara eksplisit meneruskan allocator adalah hal umum, sehingga cocok dengan cara Roc mengelola memori
- Ekosistem Rust lebih cocok untuk struktur yang diinginkan Bun, sedangkan ekosistem Zig lebih cocok untuk struktur yang diinginkan Roc
-
Penggunaan ulang serializer LLVM bitcode
- LLVM adalah dependensi inti optimizer Roc, dan melakukan optimasi tambahan setelah optimasi milik Roc sendiri
- LLVM sering merusak kompatibilitas API utama, sehingga upgrade versi memerlukan waktu dan biaya yang besar
- Format bitcode serialisasi LLVM stabil dan kompatibel ke belakang, sehingga penggunaan serializer sendiri memungkinkan lepas dari perubahan API C++
- Untuk itu dibutuhkan serializer bitcode manual yang terpisah dari library LLVM C++
- Implementasi yang sudah diketahui ada di compiler Zig, dan compiler baru Roc menggunakan ulang kode Zig tersebut
- Sumber dependensi terbesar yang Roc dapatkan dari ekosistem Zig bukan package umum, melainkan compiler Zig itu sendiri
Fitur Rust yang dirindukan di Zig
- Dalam implementasi compiler, kontrol alokasi eksplisit memang diperlukan, tetapi untuk pengujian alokasi dan dealokasi otomatis milik Rust terasa lebih nyaman
- Test allocator Zig bisa menemukan memory leak dan juga mendeteksi kebocoran pada kode Roc yang sudah dikompilasi
- Sebagai gantinya, setiap pengujian harus menulis
initdandefer deinitdengan tepat, dan jika satu saja salah pengujian akan gagal karena kebocoran
comptimedi Zig memang tumpang tindih dengan polimorfisme parametrik dan ad hoc, tetapi kedua bentuk polimorfisme itu tetap dirindukan- Trait
Allocatordi Rust bisa menerimaself - Implementasi seperti
ArenaAllocatordi Zig harus menerima pointeranyopaquelalu melakukan cast kembali ke tipenya sendiri
- Trait
- Tidak adanya field struct privat berarti field yang seharusnya tidak diakses langsung tidak bisa dicegah lewat error kompilasi
- Dalam review perubahan kode, yang terlihat hanya akses field dan dokumentasi struct aslinya tidak tampak, sehingga harus dicek terpisah setiap kali
- Konsistensi Rust yang memakai
snake_caseuntuk fungsi, variabel, dan konstanta kadang dirindukan unsafedan borrow checker memang punya biaya, tetapi juga memberi rasa aman karena masalah tertentu cukup dipikirkan di dalam blokunsafesaja- Ini bukan berarti Zig perlu menambahkan fitur yang sama
- Di Zig, dead code lebih sering ditemukan terlambat dibanding di Rust
- Tool bawaan Zig dan
tidy.zigmilik TigerBeetle juga tidak bisa menangkap sebagian dead code - Dead code tidak dihasilkan ke binary sehingga tidak berdampak ke pengguna, tetapi kurang baik untuk pengelolaan codebase
- Tool bawaan Zig dan
- Upgrade minor version dan perubahan edition di Rust umumnya sangat lancar
- Saat ini Zig memang tidak menargetkan kompatibilitas ke belakang, dan itu sesuai ekspektasi sehingga bukan masalah besar, tetapi pengalaman upgrade sederhana di Rust tetap lebih baik
Hal yang memuaskan di Zig
- Zig memiliki daya tarik desain subtraktif, yang mengurangi alat-alat yang familier seperti dalam pemrograman fungsional untuk mendapatkan sifat lain sebagai gantinya
- Tidak ada makro, dan banyak masalah termasuk polimorfisme parametrik bisa diselesaikan dengan
comptimeatau fungsi biasa - Tata letak data bisa dikendalikan dengan sangat rinci
- Tipe integer non-pangkat dua seperti
u7danu5bisa dipakai tanpa penanganan bit terpisah - packed struct didukung secara bawaan
- Fungsi bisa di-inline dari lokasi pemanggilan, bukan lokasi deklarasi
- Fitur yang di Rust memerlukan crate berbasis makro bisa dipakai tanpa dependensi tambahan
- Tipe integer non-pangkat dua seperti
- Toolchain build Zig sangat cocok untuk menghasilkan binary mandiri untuk Alpine Linux dan WebAssembly
- Build khusus yang mengompilasi builtins setara standard library Roc sebagai binary blob opak lalu menyertakannya ke executable akhir juga bisa ditangani
- Uber juga memakai toolchain Zig untuk infrastruktur build meski tidak menggunakan bahasa Zig itu sendiri
- Dalam penanganan error Zig, kegagalan alokasi heap juga diperlakukan sebagai error ruang pengguna biasa
- Roc juga memakai strategi serupa dengan union type anonim dan payload sehingga error terakumulasi secara alami
- Pendekatan Zig dan Roc lebih disukai daripada penanganan berbasis
Resultbawaan Rust,anyhow, atauthiserror - Untuk sintaks propagasi error, operator postfix
?milik Rust lebih disukai daripadatrydi Zig, sehingga Roc juga mengadopsi operator postfix? - Secara keseluruhan, termasuk API allocator yang terperinci dan kode yang bisa dipakai ulang untuk compiler berperforma tinggi, pilihan menggunakan Zig sangat memuaskan untuk proyek ini
Langkah berikutnya untuk Roc
- 0.1.0 dari compiler baru direncanakan rilis pada paruh akhir tahun ini, dan akan menjadi rilis bernomor pertama Roc
- Sebelum rilis, Nightly build sudah bisa dicoba, tetapi saat ini masih ada banyak bug, fitur yang belum selesai, dan dokumentasi yang belum lengkap
- Roc Programming Language Foundation adalah organisasi nirlaba 501(c)(3) di Amerika Serikat, dan donasi terutama digunakan untuk kompensasi kontributor
- Perkembangan pengembangan selanjutnya dan pertanyaan dapat diikuti di Roc Zulip
2 komentar
Opini di Lobste.rs
Secara angka, 35ms milik Zig hampir 100 kali lebih cepat daripada 3,4 detik milik Rust, dan jumlah kodenya juga sekitar 50% lebih banyak, tetapi dalam pengembangan nyata perbedaannya tampak agak dibesar-besarkan
Jika kompilator tidak dibangun ulang setiap menit melainkan setiap 10 menit, waktu yang dihemat hanya sekitar 3 detik, dan 3,4 detik juga sudah cukup cepat
Saat codebase membesar, Rust bisa melambat, tetapi waktu cold build justru memburuk, dan jika ditambah fakta bahwa laptop diganti setiap dua tahun serta ada peningkatan pada kompilator itu sendiri, sulit untuk menyimpulkan bahwa waktu build Rust akan terus meningkat dalam jangka panjang
Mode
checkRust dan LSP memang lebih cepat daripada kompilasi ulang penuh, tetapi belum setara dengan ituDengan kombinasi
--watch -fincremental, rekompilasi bisa dilakukan setiap kali menyimpan sehingga umpan balik datang sangat cepat dan sangat seringSaya memindahkan proyek pribadi dari Rust ke Zig dan juga telah memakai Rust untuk pekerjaan selama bertahun-tahun; kecepatan kompilasi Zig benar-benar terasa melegakan
35ms terdengar sangat cepat sampai mengejutkan
Hanya relinking saja rasanya akan memakan waktu lebih lama dari itu, jadi saya penasaran apa sebenarnya yang dilakukan kompilator tersebut
Jika implementasi fungsi berubah, assembly baru tinggal disisipkan ke biner yang ada, tetapi inlining, perubahan signature fungsi, dependensi, atau relokasi saat ruang tidak cukup memerlukan analisis tambahan
Saat fungsi diubah atau ditambahkan, ia mencari ruang kosong yang cukup besar di bagian
.textpada executable keluaran lalu menuliskan machine code baru di sanaJika ruangnya tidak cukup,
.textmungkin harus diperluas dan data lain dipindahkan, tetapi biaya ini disebar dengan memperluas section secara eksponensialJika perlu, tabel simbol dan entri relokasi juga ditambahkan, dan ruang kosong untuk itu pun sudah disisakan sebelumnya, jadi setelah pekerjaan selesai cukup tutup file
Berdasarkan hasil Tracy yang terakhir saya lihat, dari total 35ms itu waktu yang dipakai linker hanya sekitar 1ms
Angka bahwa
unsafedipakai sekitar 1.200 kali dalam 300 ribu baris kode Rust terasa lebih banyak dari perkiraanSebagai contoh, Inko hanya memiliki 162 ekspresi
unsafe { ... }, dan 87 fungsiunsafeyang terekspos ke kode hasil generate melalui C ABI, sementara total kode Rust-nya sekitar 88 ribu barisNamun, karena cara kerja Inkwell, seluruh backend LLVM pada dasarnya adalah satu area
unsafebesar, jadi sulit menarik kesimpulan hanya dari perbandingan sederhanaSaya menandai sebagai
unsafehanya fungsi yang benar-benar merusak keamanan memori, seperti ketika menangani pointer secara langsung, tetapi ada pengembang lain yang memakainya sebagai penanda bahwa langkah tertentu harus dilakukan lebih dulu agar tidak panicAngka bahwa
unsafemuncul 40 ribu kali di pustaka standar dan kompilator Rust tidak akuratItu hasil penghitungan yang bahkan memasukkan kata tersebut di dalam test dan komentar, dan sebagian besar ada di pustaka standar; pada kompilatornya sendiri jumlahnya kurang dari 2.000 bahkan jika komentar dan test ikut dihitung
Selama berkontribusi ke rustc, proporsi PR saya yang berisi
unsafebahkan tidak sampai 1%, sehingga kode tidak aman di dalam kompilator tergolong jarangWajar jika pustaka standar yang mengimplementasikan runtime dasar yang menjadi fondasi seluruh Rust memiliki banyak
unsafe, dan kode seperti ini, di bahasa apa pun, tetap tidak aman baik secara eksplisit maupun implisitKompilator tersebut ukurannya sekitar 10 kali lebih kecil daripada rustc, tetapi penggunaan
unsafe-nya mirip, jadi mungkin belum bisa dibilang merajalela, namun cukup sering ditemui, dan saya penasaran mengapa kebutuhannya jauh lebih besar daripada di rustcKami memang tidak menganalisis secara terpisah
unsafeyang terkait pustaka standar di kedua codebase, tetapi jika kompilator baru ini ditulis dalam Rust, saya memperkirakan jumlahunsafe-nya akan lebih banyak daripada rustc karena struktur yang memakai cache dan indeks alih-alih pointerTidak ada niat meremehkan pekerjaan tim Rust; kami hanya ingin menjelaskan pilihan dan perkembangan kami sambil tetap menghormati pencapaian proyek lain
Ini tulisan yang menilai kelebihan dan kekurangan Rust dan Zig secara seimbang tanpa menutup-nutupi, dan kesimpulannya terasa sangat jujur: bahkan setelah menulis ratusan ribu baris kode selama 18 bulan dan menangani ratusan bug, hasil proyek kemungkinan tidak akan banyak berubah meski mereka memilih bahasa lain di antara opsi yang ada
Namun, penjelasan bahwa dalam compiler yang menghasilkan kode mesin seperti Roc atau rustc, operasi yang tidak aman terhadap memori merupakan bagian besar dari pekerjaan terasa sulit dipahami
Ada banyak compiler yang ditulis dengan OCaml atau Haskell, dan pembuatan kode mesin sendiri pada dasarnya hanya menyusun byte ke dalam vektor lalu menuliskannya ke file, jadi tidak ada alasan itu harus tidak aman
Jika untuk interpreting atau kompilasi JIT saya bisa memahaminya, tetapi saya penasaran mengapa itu juga diperlukan dalam kompilasi biasa
Risikonya muncul saat kode mesin yang dihasilkan dijalankan, dan banyak compiler nyata sering kali menghasilkan sekaligus menjalankan kode mesin, sehingga disebut sebagai bagian besar dari pekerjaan
Selain interpreting dan JIT, contohnya mencakup evaluasi kode pengguna pada waktu kompilasi seperti
const fndi Rust atau ekspresi yang bisa diangkat ke tingkat teratas di Roc, atau menjalankan tes lalu memeriksa output untuk menentukan apa yang akan ditampilkan kepada penggunaBaik memori yang rusak berada di proses compiler maupun di program hasilnya, penyebab prosesor berperilaku salah tetaplah instruksi yang dibuat compiler, dan tempat yang harus diperbaiki juga menurut kriteria itu adalah kode compiler
Komponen seperti kode tautan eksternal dan garbage collector juga membutuhkan banyak
unsafedi Rust, sehingga cukup banyak menghapus keuntungan dari Rust yang amanCompiler memang bisa saja ditulis hanya dalam bahasa yang aman, tetapi performa menjadi masalah, dan Zig serta compiler Roc banyak menggunakan struktur array (SoA) dan indeks array alih-alih pointer
Jika ini diimplementasikan dalam Rust, pemeriksa borrow akan terpaksa diakali sehingga keuntungan keamanan terkait pun hilang
Relasi lifetime dalam compiler ternyata cukup sederhana: pada satu tahap, data dialokasikan, dibuat, dan dimodifikasi di arena, lalu diteruskan sebagai hanya-baca ke tahap berikutnya, setelah itu seluruh arena bisa dibuang
Bagian yang lebih rumit adalah kompilasi inkremental dan linking yang membaca state dari disk dan memodifikasi biner di tempat; dalam proses ini bisa muncul kerusakan state, bug, salah kompilasi, dan masalah memori, tetapi itu terpisah dari keamanan memori proses compiler itu sendiri
Keamanan dan ketepatan adalah konsep yang jauh lebih luas daripada keamanan memori di dalam program, terutama jika ingin melakukan pekerjaan yang sulit diekspresikan dalam Rust dengan aman dan benar
Senang melihat
compact_arenadiperkenalkan dengan menyebut pembuatnya, tetapi cara penggunaanunsafedi library ini digambarkan secara keliruTujuan
compact_arenaadalah menjamin dalam Rust yang aman bahwanewtidak bisa disalahgunakan, sehingga pengindeksan menjadi amanMakro
mk_arenabisa dipanggil dari kode Rust yang aman bahkan di dalam loop ketika jumlah arena yang dibutuhkan tidak diketahuinewdigunakan dengan aman, tetapi pada bagian keamanan di https://docs.rs/compact_arena/0.5.0/… tertulis bahwa tag yang diberikan ke konstruktor adalah dasar dari teknik pengindeksan, dan jika nilai ini digunakan pada arena lain maka indeks dari dua arena bisa tercampur, menyebabkan akses di luar batas dan perilaku tak terdefinisiInti poin saya bukan pada pemanggilan
new()itu sendiri, melainkan bahwa risikonya muncul saat melakukan pengindeksan pada nilai yang dibuat dengannew(), dan menurut saya bagian yang perlu diaudit dan diberi tandaunsafeadalah lokasi pemanggilan pengindeksan yang sebenarnyaNamun, mungkin saja saya salah memahami desainnya
Setahu saya, Python mengharuskan program di-restart untuk mencerminkan kode baru, dan hot reloading memerlukan ekstensi nonstandar serta penulisan kode yang hati-hati
Hot reloading lebih umum di lingkungan pengembangan berbasis image seperti Lisp atau Smalltalk, atau di Erlang
importlib.reload(), hot reloading juga dimungkinkan di Python, tetapi saya tidak tahu seberapa luas itu digunakanSaya penasaran apakah tidak menggunakan
cargo buildbisa mengurangi waktu buildTidak jelas apakah pekerjaan menyiapkan Bazel lebih besar atau lebih kecil daripada memindahkan ke Zig, dan tentu saja waktu build bukan satu-satunya alasan pergantian bahasa
Komentar Hacker News
Secara umum tulisannya bagus, tetapi sulit setuju dengan pernyataan bahwa dalam compiler yang menghasilkan kode mesin seperti Roc atau rustc, operasi yang tidak aman terhadap memori merupakan bagian besar dari pekerjaan
Patch biner saat berjalan atau pemuatan ulang kode mungkin memerlukan kode tidak aman, tetapi dalam proses biasa membuat executable, tidak ada alasan pembuatan kode mesin itu sendiri harus tidak aman
Justru kemungkinan menemukan kode tidak aman lebih besar di runtime bahasa tersebut
Namun dalam praktiknya banyak compiler yang tidak hanya menghasilkan kode mesin tetapi juga langsung mengeksekusinya, jadi saya menyebutnya “bagian besar dari pekerjaan”, meski compiler tidak harus melakukan kedua hal itu sekaligus
Yang saya bayangkan bukan hanya patch biner, pemuatan ulang kode, dan runtime, tetapi juga mengevaluasi kode pengguna pada waktu kompilasi, seperti
const fndi Rust atau ekspresi yang bisa diangkat ke tingkat teratas di Roc, serta menjalankan test dan memeriksa output untuk menentukan apa yang akan ditampilkanLogikanya mirip dengan mengatakan bahwa karena kepala masih bisa terbentur meski memakai sabuk pengaman, maka sabuk pengaman hanya merepotkan dan sebaiknya tidak dipakai
Pada kode berperforma sangat ekstrem, struktur data, algoritme, hingga strategi alokasi memori pun berubah, dan TigerBeetle terkenal karena mengalokasikan seluruh memori sekaligus saat mulai berjalan
Compiler Roc juga tampaknya ingin mengambil kompromi yang mirip dengan Zig, jadi wajar jika banyak pola umum yang muncul
Saya tidak melihat dasar untuk klaim bahwa
ReleaseSafemenangkap use-after-free dengan pemeriksaan runtimeSaya sudah menelusuri dokumentasi Zig tentang pemeriksaan keamanan memori saat runtime, tetapi tidak menemukan hal terkait meski mencari
use-after-free,UaF, atausafety-checked; bahkan jika memakaiDebugAllocatorpada build rilis pun, hal itu tidak terdeteksi secara andalTulisan terkait dirangkum di https://landaire.net/memory-safety-by-default-is-non-negotia...
Ini penilaian yang saya dapat dari menulis kode sendiri sebelum era AI, dan mungkin ada sesuatu yang berbeda dalam kombinasi Zig dan LLM, tetapi semakin dibaca semakin banyak klaim yang terasa janggal dalam cara penyampaiannya sehingga sulit dipercaya
Ini terasa bukan seperti penilaian teknis yang jujur, melainkan tulisan untuk membenarkan perdebatan yang sudah ada; tetapi karena saya menyukai tulisan dan bahasa yang unik serta punya sedikit antipati terhadap demam AI yang berlebihan, saya mencoba melihatnya dengan niat baik
ReleaseSafehanya menambahkan pemeriksaan batas dan memicu panic pada kode yang tidak dapat dicapaiZig tampaknya tidak menyediakan temporal memory safety
Menarik bahwa OCaml yang sudah matang, fleksibel, dan cukup ekspresif dipakai untuk memvalidasi purwarupa, tetapi tidak dipilih sebagai bahasa implementasi akhir
Saya juga ragu incremental build Zig akan benar-benar jauh lebih cepat daripada dune, dan meski cross-compilation adalah keunggulannya, hal itu juga tidak dibahas dalam “mengapa Zig”
Saya penasaran apakah kontrol memori yang rinci memang sepenting itu bagi compiler, dan pada tonggak seperti apa para maintainer memutuskan beralih ke bahasa lain, seperti Rust dan WASM yang dimulai dengan OCaml
Incremental build Zig jelas merupakan fitur penentu, dan saya bisa memahami keputusan mengganti bahasa demi mendapatkannya dalam jangka pendek
Tetapi dalam jangka menengah, saya berharap Rust juga akan mendapatkan fitur serupa dalam waktu dekat
Saya menginginkan kecepatan, tetapi tidak ingin bergerak cepat lalu celaka; saya sedang membuat bahasa yang menggabungkan keamanan Rust, fitur Zig, dan runtime Go tanpa garbage collector
Arah ini tampak lebih mungkin diwujudkan, dan sepertinya juga bisa diimplementasikan di ruang pengguna
Akan jauh lebih meyakinkan jika mereka benar-benar melakukan perbandingan ilmiah saat memilih bahasa implementasi kompiler
Tampaknya mereka berangkat dari asumsi yang belum terverifikasi bahwa kompiler berperforma tinggi memerlukan bahasa sistem tingkat rendah (https://www.roc-lang.org/faq#self-hosted-compiler), lalu menyimpulkan bahwa satu-satunya pilihan selain Rust adalah Zig
Performa kompiler ditentukan oleh algoritme, dan bahkan bahasa terkelola yang cepat umumnya tetap berada dalam kisaran dua kali waktu eksekusi jika algoritmenya sama, sedangkan kesenjangan performa akibat perbedaan algoritme tidak punya batas seperti itu
Zig sendiri merupakan kontracontoh bagi teori bahwa menulis kompiler dalam bahasa tingkat rendah akan membuatnya lebih cepat, dan sekitar 15 ribu baris per detik milik Roc bukanlah angka yang cepat. Ada juga referensi bahwa pada 1998 kompiler ML sudah memproses 3 ribu baris per detik (https://flint.cs.yale.edu/cs421/case-for-ml.html)
Mungkin akan lebih membantu untuk masa depan jika pekerjaan kompiler saat ini dihentikan dulu dan dibuat kompiler self-hosting untuk subset Roc sekecil mungkin yang bisa dilakukan dalam kurang dari 10 ribu baris
Dengan begitu, alih-alih implementasi 300 ribu baris, berbagai implementasi bisa diuji pada skala 10 ribu baris, dan dapat diverifikasi apakah bahasa tingkat rendah benar-benar diperlukan untuk target performa yang diinginkan
Dalam proses self-hosting, fitur Roc yang benar-benar penting akan terlihat, lebih banyak kode Roc juga akan ditulis, dan jika fitur umum yang dibutuhkan kompiler diperbaiki maka aplikasi di lapisan bawah pun ikut membaik
Sulit menilai kecepatan kompilasi Roc saat ini hanya dari fakta bahwa ML bisa dikompilasi cepat pada 1990-an. Desain bahasa memberi batasan kuat pada algoritme yang dibutuhkan, dan perangkat keras modern juga jauh lebih kompleks
Roc tampaknya memiliki tingkat overloading tertentu dan juga algoritme canggih untuk menghindari alokasi closure di heap, dan kebutuhan seperti ini bisa menciptakan kompleksitas algoritmik yang tidak bisa dihilangkan
Jika batas optimasi algoritme sudah tercapai, yang tersisa adalah pengurangan faktor konstan, dan khususnya bahasa tingkat tinggi yang mengelola memori jelas memiliki batas bawah dalam seberapa jauh faktor itu bisa ditekan
Saya pernah melihat kasus di kode nyata di mana performa meningkat lebih dari 10x hanya dengan mengendalikan tata letak memori secara langsung, dan di industri game pekerjaan seperti ini bahkan bisa memakan porsi besar dalam karier. Lingkungan di mana satu algoritme cerdas menghapus semua masalah performa terasa jauh dari kenyataan
Build Rust adalah salah satu penyebab pemborosan ruang penyimpanan terbesar di hampir semua komputer, dan jika membangun beberapa pustaka maka puluhan GB cepat sekali menumpuk
Memang bisa mengatur folder build global agar dependensi dipakai ulang antarpoyek, tetapi solusi apa pun sebaiknya disediakan sebagai perilaku bawaan
Namun cache tetap perlu garbage collection, dan pekerjaan penataan baru untuk artefak build perantara agar itu mudah dilakukan sudah mendekati tahap akhir
node_modulesselalu dikritikBahkan pada proyek Tauri, yang kode backend-nya jauh lebih kecil daripada frontend, artefak build Rust mencapai 9GB, sementara
node_moduleshanya 550MBSaya memahami Roc sebagai bahasa scripting yang disisipkan ke C ABI, dan saya penasaran dengan bidang penggunaan nyatanya
Apakah Roc ingin bersaing dengan WASM dalam lingkungan plugin yang menyediakan platform Roc besar, dan mengapa pengembang aplikasi memilih mengekspos lapisan Roc alih-alih WASM yang memungkinkan pengembang plugin memakai bahasa apa pun?
Jika ini bahasa tingkat aplikasi yang memakai platform Roc kecil, saya juga penasaran apakah ia ingin bersaing dengan Gleam untuk kode HTTP sisi server dan Elm untuk kode klien
Waktu kompilasi adalah faktor yang sangat diremehkan. Menunggu build C++ selama 10 menit adalah keluhan terbesar saya, dan itu benar-benar memutus fokus pengembangan
Sampai-sampai saat berpindah dari file
.rske file.ts, rasanya seperti mengganti komputer itu sendiriSaya tidak memakai Zig secara langsung, tetapi saya sangat berharap pada beberapa kemungkinannya
Game baru yang ditulis dengan Zig yang lebih nyaman dipakai daripada C, perangkat lunak terdistribusi yang potensinya sudah ditunjukkan TigerBeetle, dan bidang robotika yang secara pribadi saya minati terasa sangat menjanjikan