Optimisasi Tingkat Rendah dan Zig
(alloc.dev)- Optimisasi tingkat rendah adalah upaya membuat compiler lebih memahami maksud dan batasan kode, dan Zig sangat cocok untuk tujuan ini karena memudahkan penjelasan tipe, alignment, aliasing, dan informasi compile-time secara eksplisit
- Compiler pengoptimasi seperti LLVM pun tidak selalu menghasilkan kode terbaik, sehingga pada bagian bottleneck tetap perlu memeriksa kode yang dihasilkan dan menyesuaikan kode sumber
- Zig dapat meneruskan
noalias,align, ukuran array tetap, dan tipe elemen pada compile-time untuk menghasilkan kode tervectorisasi yang lebih kecil dibanding contoh JavaScript comptimemenjalankan kode Zig biasa pada compile-time, memungkinkan metaprogramming seperti pembuatan konstanta, implementasi generik, refleksi tipe, dan optimisasi perbandingan string- Kekuatan Zig terletak pada eksekusi compile-time yang terintegrasi ke bahasa, bukan makro yang mengubah AST secara langsung; sebagian nilai runtime juga dapat didispatch ke fungsi spesialisasi compile-time
Mengapa compiler perlu dipercaya, tetapi tetap diverifikasi
- Optimisasi bukan sekadar teknik untuk membuat program cepat; ia juga terkait dengan pengurangan biaya, peningkatan skalabilitas, dan menjaga kesederhanaan sistem
- Compiler modern menghasilkan keluaran mengesankan dengan backend seperti LLVM, tetapi dalam beberapa situasi masih menghasilkan kode yang kurang optimal
- Bahasa tingkat rendah cepat bukan hanya karena overhead garbage collection atau interpreter lebih kecil, melainkan juga karena dapat mengekspresikan lebih banyak informasi maksud yang bisa dipahami compiler
- Compiler tidak dapat mengubah algoritme atau paradigma pemrograman itu sendiri, dan umumnya melakukan optimisasi dalam lingkup terbatas seperti loop
Contoh nilai maksimum array di JavaScript dan Zig
- Contoh JavaScript menyimpan nilai maksimum per elemen dari dua array ke
xdalam bentukx[i] = y[i] > x[i] ? y[i] : x[i] - Bagi manusia kodenya jelas, tetapi bytecode yang dihasilkan V8 membengkak
- Contoh Zig menjelaskan informasi yang diperlukan untuk optimisasi secara lebih spesifik pada argumen fungsi
noalias x:xbukan alias dari pointer lain*align(64): alignment 64 byte[65536]f64: ukuran array dan tipe elemenconst: argumen hanya-baca
- Berkat informasi ini, compiler dapat membuat kode yang lebih baik; dalam contoh tersebut dihasilkan assembly tervectorisasi
- Kode Rust yang setara juga menghasilkan assembly yang hampir sama
Titik keunggulan Zig untuk optimisasi dan batasannya
- Zig mengizinkan ekspresi yang verbose sehingga banyak informasi kode dapat diteruskan ke LLVM
- Elemen utama yang disediakan Zig terkait optimisasi adalah sebagai berikut
- Model memori Rust memungkinkan compiler selalu berasumsi bahwa argumen fungsi tidak membuat alias, tetapi di Zig hal ini harus dinyatakan langsung
- Jika compiler tidak dapat mengetahui bahwa argumen fungsi Zig tidak memiliki alias, fungsi Zig tanpa anotasi bisa lebih lambat daripada fungsi Rust
- Bahkan jika hanya berpatokan pada LLVM IR yang dianotasi dengan baik, Zig memberikan hasil bagus, tetapi kekuatan yang lebih besar ada pada eksekusi compile-time
Peran comptime
comptimedi Zig adalah fitur untuk pembuatan kode pada compile-time- Hal-hal yang dapat dilakukan pada compile-time meliputi
- Membuat konstanta dan menyertakannya ke dalam binary
- Menghindari penulisan ulang struktur hashmap yang sama untuk berbagai tipe data
- Mendorong optimisasi agar menghapus kode yang tidak perlu berdasarkan data yang diketahui pada compile-time
- Memeriksa, merefleksikan, dan membuat tipe untuk mengimplementasikan generik
- Kode
comptimeadalah kode Zig biasa yang dijalankan pada compile-time, dan tidak boleh memiliki efek samping seperti network IO - Mesin emulasi pada compile-time sesuai dengan target kompilasi
- Hampir semua kode Zig dapat dijalankan pada compile-time dengan
comptime, dan semua tipe dapat diperiksa, direfleksikan, serta dibuat pada compile-time
Perbedaannya dengan makro
- Tujuan
comptimemirip dengan makro, tetapi cara kerjanya berbeda - Sebagian makro mengganti teks mentah, sementara sebagian lain langsung memodifikasi AST program
comptimeZig tidak mengubah AST secara langsung, dan juga tidak memiliki fitur seperti makro penyambung token- Zig menargetkan bahasa yang mudah dibaca, sehingga tidak cocok dengan gaya makro yang membuat atau memodifikasi variabel di scope yang tidak terkait
- Hal-hal yang dapat dilakukan makro tetapi tidak dapat dilakukan langsung oleh
comptimeZig adalah sebagai berikut- Mendefinisikan makro lain
- Mengubah AST
- Mengimplementasikan mini-language atau DSL secara langsung
- Namun DSL tetap dapat dibuat di Zig; fungsi
printZig mem-parse string format dengancomptimeuntuk menyusun graph fungsi yang akan menserialisasi data - Contohnya adalah TigerBeetle account testing DSL, comath, dan zilliam
Optimisasi perbandingan string dengan comptime
- Perbandingan string umum mengembalikan
falsejika panjangnya berbeda, dan jika panjangnya sama membandingkan tiap byte secara berurutan - Cara ini harus membaca byte dari kedua string lalu membandingkannya
- Sering kali salah satu string sudah diketahui pada compile-time, sehingga di Zig satu argumen dapat diwajibkan sebagai
comptimefn staticEql(comptime a: []const u8, b: []const u8) bool
- Saat membandingkan dengan string statis seperti
"Hello!\n", compiler menghasilkan kode yang terdiri dari perbandingan panjang dan perbandingan konstanta untuk tiap byte - Tujuan bagian ini bukan sekadar menunjukkan optimisasi yang bisa dilakukan compiler secara otomatis, tetapi menunjukkan bahwa transformasi dapat dipaksa dengan
comptimeuntuk membuka peluang yang tidak terlihat oleh compiler
Perbandingan unit lebih besar dan pemanfaatan SIMD
- Perbandingan string sederhana dengan
comptimemasih melakukan perbandingan per byte - Versi yang ditingkatkan menentukan ukuran blok perbandingan dengan
std.simd.suggestVectorLength(u8)atau@sizeOf(usize) - Setelah memeriksa panjang string terlebih dahulu, jumlah blok besar yang dapat dibandingkan dan jumlah byte tersisa dihitung
- Setiap blok di-
@bitCastke tipe integer yang dibuat denganstd.meta.Int(.unsigned, block_len * 8)lalu dibandingkan - Byte yang tersisa juga dibandingkan dengan tipe integer terpisah
- Assembly yang dihasilkan pada contoh
"Hello, World!\n"menggunakan register yang lebih besar dan mengurangi jumlah percabangan kondisional - Pada perbandingan string yang lebih besar, dihasilkan assembly yang menggunakan register SIMD lebih besar
Menggabungkan nilai runtime dan spesialisasi compile-time
comptimeZig tidak terbatas hanya pada data yang diketahui pada compile-time- Dalam kasus sederhana, beberapa prosedur dapat dibuat pada compile-time, lalu dilakukan dynamic dispatch ke prosedur yang sesuai berdasarkan nilai runtime
- Kode contoh mengirim nilai dalam rentang
inline 0...100dariswitch (runtime_val)kestaticFn(comptime_val), sementara sisanya ditangani olehruntimeFn(runtime_val) - Jika tidak ingin ukuran binary bertambah, dapat fallback ke implementasi runtime penuh
Kesimpulan
comptimeZig berperan sebagai pengganti template, makro, generik, dan pembuatan kode manual- Hal serupa dapat dilakukan di bahasa lain, tetapi di Zig
comptimeterintegrasi lebih alami ke dalam bahasa - Zig memudahkan penulisan kode berperforma baik dalam situasi yang benar-benar berguna, berlawanan dengan Turing tar-pit tempat segala hal mungkin dilakukan tetapi pekerjaan menarik sulit dikerjakan
- Tentang perang bahasa, tetap ada dua pandangan sekaligus: perspektif besar bahwa Turing completeness saja sudah cukup, dan bahwa orang-orang tetap bisa memiliki bahasa yang mereka sukai
- Pernyataan yang menjadikan bahasa itu sendiri sebagai target benchmark, seperti “C lebih cepat daripada Python”, bisa keliru; target benchmark sebenarnya bukan bahasa, melainkan kode dan implementasi tertentu
1 komentar
Opini Hacker News
Hal yang paling menarik dari Zig bagi saya adalah kemudahan sistem build, cross-compilation, dan tujuannya untuk mendukung iterasi yang cepat
Sebagai pengembang game saya memang punya kebutuhan performa, tetapi menurut saya sebagian besar bahasa sudah memberikan performa yang cukup, jadi itu bukan prioritas nomor satu dalam memilih bahasa
Kode yang kuat bisa ditulis dengan bahasa apa pun, tetapi kuncinya adalah memilih framework yang sangat siap menghadapi masa depan untuk menjaga kode modular selama puluhan tahun
C/C++ selama ini menjadi jawaban default karena didukung di mana-mana, dan Zig terasa seperti bisa mencapai level itu
Zig bersikap tidak ramah terhadap enkapsulasi, dan anggota struct tidak bisa dibuat privat: https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
Kutipan intinya adalah sikap bahwa “field privat dan getter/setter adalah antipola yang dipopulerkan Java; karena field adalah data yang memang ada, beri nama dengan hati-hati dan dokumentasikan sebagai bagian dari API publik”
Jika representasi internal tidak bisa disembunyikan, sulit membuat kontrak API yang baik, padahal itu adalah dasar modularitas perangkat lunak; seharusnya kita bisa mengubah representasi internal tanpa merusak kode pengguna
Sikap Zig tampaknya adalah bahwa tidak semestinya ada sesuatu yang disebut representasi internal terpisah, dan representasi itu sendiri harus dipublikasikan, didokumentasikan, serta dijamin; saya berharap suatu hari keputusan ini dibalik dan field privat didukung
Banyak hal langsung berjalan, dan saya bahkan bisa men-debug bug aneh dengan GDB yang sudah tua
Saya juga jadi yakin dengan Zig, dan menulis soal itu di sini: https://news.ycombinator.com/item?id=44211041
Saya tidak begitu paham mengapa orang begitu membencinya
Generic yang tidak enak dilihat juga ada di C# atau TypeScript, dan borrow checker adalah konsep yang masuk akal jika pernah mengerjakan pekerjaan level rendah
Di antara tool yang dibuat di atas Zig, yang benar-benar membuat saya kagum adalah bun, dan setelah memakai bun hidup saya menjadi jauh lebih sederhana
Hal serupa juga bisa saya katakan tentang uv yang dibuat dengan Rust
Biasanya konsol tidak menyukai hal selain C/C++, tetapi karena Zig bisa ditranspilasi ke C, mungkin tidak sepenuhnya dikesampingkan
Mengenai klaim bahwa “bahkan compiler modern pun melanggar spesifikasi bahasa (Clang mengasumsikan semua loop tanpa efek samping akan berakhir)”, saya tidak meragukan bahwa compiler sesekali melanggar spesifikasi, tetapi dalam kasus itu Clang setidaknya benar menurut standar sejak C11
Di C11 tertulis bahwa loop yang ekspresi kontrolnya bukan ekspresi konstan, dan tidak melakukan I/O, akses volatile, sinkronisasi, atau operasi atomik, boleh diasumsikan oleh implementasi akan berakhir
Karena itu, loop tak hingga sederhana di C seperti
for (;;);seharusnya benar-benar dikompilasi sebagai loop tak hingga, danloop {}milik Rust yang tidak terlalu opak juga seharusnya begituNamun LLVM dibuat oleh orang-orang yang kadang lupa bahwa mereka tidak selalu membuat compiler C++, sehingga pernah ada titik ketika Rust berkata “tolong beri saya loop tak hingga”, LLVM menjawab “menurut C++ hal semacam itu tidak ada, jadi akan saya optimalkan”, dan itu merupakan penerapan yang keliru pada bahasa lain
Untuk melakukan inline dan unroll pada perbandingan string, comptime tidak mutlak diperlukan
Itu juga bisa dilakukan di C: https://godbolt.org/z/6edWbqnfT
Typo sudah diperbaiki
Contoh yang lebih baik adalah https://github.com/RetroDev256/comptime_suffix_automaton
Namun kode godbolt yang Anda tautkan sebenarnya juga menunjukkan salah satu dari dua contoh yang kurang bagus
Menurut saya, membandingkan contoh JavaScript dengan contoh Zig/Rust bukan perbandingan yang bagus
Kompiler Zig dan Rust diminta memilih CPU target yang sangat modern, sementara V8 tampaknya tidak berada pada kondisi yang sama
JIT yang mengoptimalkan juga bisa melakukan vektorisasi jika kondisinya tepat
Sebagai catatan, sebagian besar bahasa modern melakukan optimisasi yang sama untuk string, dan contoh C++ ada di sini: https://godbolt.org/z/TM5qdbTqh
Contoh Zig memakai array bertipe yang diketahui dengan ukuran tetap, sedangkan kode JS bersifat “generik” saat runtime, sehingga
xdanybisa berupa objek apa punDi JS memang harus membayar biaya itu, tetapi ironisnya pada contoh khusus ini kita bisa menyampaikan informasi tipe ke JIT dengan lebih baik
Jika fungsi ini selalu dipanggil dengan Float64Array berukuran sama, JIT bisa mengetahuinya dan membuat loop yang lebih cepat. Meski bukan vektorisasi, hasilnya jauh lebih baik
Namun typed array punya biaya inisialisasi yang besar, jadi dalam praktiknya jarang dipakai kecuali Anda mengalokasikan satu typed array besar lalu menggunakannya ulang berkali-kali
Selain itu tulisan tersebut mengatakan bytecode JS cukup membengkak; kemungkinan besar karena JIT tidak bisa menjamin bahwa
65536sama dengan panjang kedua array, sehingga ia memasukkan guardMeski begitu, dalam praktiknya tidak ada orang yang menulis loop
forseperti itu; mereka akan menulisi < x.length, dan dalam kasus ini JIT setidaknya menghapus satu pemeriksaan arraytargetbisa diganti ke CPU yang lebih lamaMaaf, saya tidak terpikir soal batasan target JS
Contoh C++ yang ditautkan adalah contoh bagus tentang apa yang bisa dilakukan Clang di C++, tetapi meski mempertimbangkan bahwa Zig mengompilasi untuk CPU tertentu, assembly yang dihasilkan tampak agak mengecewakan
Akan sangat menarik melihat port C++ dari https://github.com/RetroDev256/comptime_suffix_automaton
Ini adalah pemanfaatan comptime yang tidak bisa diinferensikan dengan rapi oleh kompiler C++
Saya tidak yakin kalimat “bahasa tingkat tinggi kekurangan intensi yang kaya seperti pada bahasa tingkat rendah” benar-benar tepat
Ekspresi intensi tampaknya bukan unsur dalam spektrum tingkat tinggi/tingkat rendah; justru semakin banyak cara untuk mengekspresikan intensi secara lebih rinci, seharusnya semakin dekat ke tingkat tinggi
Ini adalah intensi tentang apa yang ingin kita buat mesin lakukan, bukan apa yang ingin dicapai
Kode seperti
purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?;penuh dengan intensi, tetapi kita tidak tahu instruksi mesin apa yang akhirnya akan dihasilkanSintaks loop
foritu mengerikanApakah maksudnya ada dua daftar berdampingan, dan posisi item di satu daftar berkaitan dengan posisi item di daftar lainnya?
Melihatnya saja membuat mata sakit
Rasanya bahasa-bahasa modern mulai tersesat ketika menambahkan berbagai “sihir” ke parser dan menebarkan simbol-simbol kecil di seluruh kode
Bukan bentuk yang ingin saya tatap berjam-jam
Jadi wajar jika Zig menyediakan sintaks yang memudahkan persis hal itu sekaligus membuat apa yang terjadi tetap terlihat jelas
Secara pribadi saya rasa ini cukup berhasil, jadi saya penasaran mengapa itu membuat mata sakit
Saya sangat menyukai model allocator Zig
Saya berharap di Go juga bisa memakai sesuatu seperti allocator per request alih-alih garbage collection
Karena bahasa itu sendiri tidak punya cara untuk mengekspresikan dan menegakkan aturan ownership, akhirnya rasanya seperti menulis C dengan sintaks yang sedikit berbeda sambil berharap semuanya berjalan baik
Tanpa garbage collection, bahkan C++ jauh lebih aman daripada Go
Model memori Rust memungkinkan kompiler selalu berasumsi bahwa argumen fungsi sama sekali tidak memiliki referensi alias, tetapi di Zig hal ini harus ditentukan secara manual
Penandaan alias seperti ini saya hindari
Karena hanya sedikit orang yang memahaminya, dan jika salah digunakan bisa menimbulkan bug yang sulit dipahami dalam kode
Mengenai klaim bahwa fleksibilitas comptime Zig membawa perbaikan bagus ke bahasa-bahasa lain, eksekusi fungsi saat kompilasi dan fungsi yang menerima argumen konstan sudah diperkenalkan D pada 2007, dan membuat beberapa bahasa mengadopsi hal serupa
https://dlang.org/spec/function.html#interpretation
Saya suka Zig, tetapi pernyataan “saya suka kepanjanglebaran Zig” terdengar aneh
C memang terlalu longgar di banyak sudut, tetapi menurut standar saat ini Zig kadang berayun sedikit terlalu jauh ke arah sebaliknya, sehingga muncul terlalu banyak noise anotasi tipe yang terasa seperti komentar
Terutama casting bilangan bulat eksplisit dalam ekspresi; saya menulis sedikit tentang itu di sini: https://floooh.github.io/2024/08/24/zig-and-emulators.html
Dari sisi performa, jika kode Zig lebih cepat daripada kode C yang serupa, biasanya itu karena pengaturan optimisasi LLVM Zig yang lebih agresif
Misalnya, secara default Zig menggunakan
-march=nativedan optimisasi seluruh program, serta mengompilasi semua kode Zig dalam proyek sebagai satu unit kompilasiHampir semua “trik” seperti memakai
unreachablesebagai petunjuk optimisasi juga bisa dilakukan di C, hanya saja kadang memerlukan ekstensi bahasa nonstandarCompiler C, terutama Clang, juga melakukan constant folding dengan sangat agresif, dan dapat memangkas area kode besar yang bisa dilipat menjadi konstanta meskipun ada call stack yang dalam
Jadi jika hanya melihat code generation, sering kali tidak banyak berbeda dari comptime di Zig
Keunggulan comptime adalah ia tidak diam-diam kembali menjadi kode runtime, dan kode yang bukan comptime pun tetap menjadi sasaran optimisasi constant folding yang sama seperti di C
Misalnya, jika fungsi non-comptime yang “murni” dipanggil dengan argumen konstanta, compiler tetap akan mengganti pemanggilan fungsi itu dengan hasilnya
Ringkasnya, jika kode C lebih lambat daripada kode Zig, Anda perlu memeriksa pengaturan compiler C. Pada akhirnya, semua pekerjaan berat optimisasi terjadi di bawah LLVM
fn signExtendCast(comptime T: type, x: anytype) T { const ST = std.meta.Int(.signed, @bitSizeOf(T)); const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x))); return @bitCast(@as(ST, @as(SX, @bitCast(x)))); }export fn addi8(addr: u16, offset: u8) u16 { return addr +% signExtendCast(u16, offset); }Ini dikompilasi menjadi assembly yang sama, bisa digunakan ulang, dan niatnya juga menjadi jelas
Saya setuju dengan pernyataan itu
Virgil sudah bisa menggunakan seluruh bahasa pada waktu kompilasi sejak 2006 dan juga mendukung kompilasi seluruh program
Namun karena Virgil tidak menargetkan LLVM, perbandingan kecepatannya pada akhirnya menjadi perbandingan dua backend compiler
Virgil sangat mengandalkan analisis keterjangkauan dan optimisasi spesialisasi yang dimungkinkan oleh model kompilasi ini
Misalnya, ia secara agresif mendevirtualisasi pemanggilan metode, menghapus field dan objek yang tidak dapat dijangkau, melakukan promosi konstanta melalui field dan objek heap, serta sepenuhnya memonomorfisasi kode polimorfik
Alasannya semata-mata karena itu membuatnya lebih mudah ditangani AI
Apakah memakai AI untuk coding adalah ide bagus, dan sejauh apa itu bagus, adalah persoalan terpisah, tetapi banyak developer percaya demikian dan bahasa-bahasa akan berusaha mengakomodasi mereka