1 poin oleh GN⁺ 2025-06-08 | 1 komentar | Bagikan ke WhatsApp
  • 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
  • comptime menjalankan 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 x dalam bentuk x[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: x bukan alias dari pointer lain
    • *align(64): alignment 64 byte
    • [65536]f64: ukuran array dan tipe elemen
    • const: 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

  • comptime di 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 comptime adalah 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 comptime mirip dengan makro, tetapi cara kerjanya berbeda
  • Sebagian makro mengganti teks mentah, sementara sebagian lain langsung memodifikasi AST program
  • comptime Zig 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 comptime Zig adalah sebagai berikut
    • Mendefinisikan makro lain
    • Mengubah AST
    • Mengimplementasikan mini-language atau DSL secara langsung
  • Namun DSL tetap dapat dibuat di Zig; fungsi print Zig mem-parse string format dengan comptime untuk 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 false jika 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 comptime
    • fn 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 comptime untuk membuka peluang yang tidak terlihat oleh compiler

Perbandingan unit lebih besar dan pemanfaatan SIMD

  • Perbandingan string sederhana dengan comptime masih 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-@bitCast ke tipe integer yang dibuat dengan std.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

  • comptime Zig 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...100 dari switch (runtime_val) ke staticFn(comptime_val), sementara sisanya ditangani oleh runtimeFn(runtime_val)
  • Jika tidak ingin ukuran binary bertambah, dapat fallback ke implementasi runtime penuh

Kesimpulan

  • comptime Zig berperan sebagai pengganti template, makro, generik, dan pembuatan kode manual
  • Hal serupa dapat dilakukan di bahasa lain, tetapi di Zig comptime terintegrasi 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

 
GN⁺ 2025-06-08
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

    • Saya suka Zig, tetapi menurut saya maintainability jangka panjang dan modularitas justru termasuk salah satu bagian terlemahnya
      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
    • Sekadar iseng, saya mencoba menjalankan Zig di perangkat Kindle lama di atas Linux 4.1.15 yang sudah dipangkas, dan itu pengalaman yang cukup menarik; saya pleasantly surprised dengan tingkat kematangan Zig
      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 sempat mencoba Rust sedikit dan menyukainya, tetapi berhenti sementara setelah mendengar reputasinya buruk; ketika saya mencobanya lagi, saya tetap menyukainya
      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
    • Zig terlihat seperti Rust yang lebih sederhana sekaligus Go yang lebih baik
      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
    • Saya penasaran bagaimana Zig akan berjalan di konsol
      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

    • C++ memang mengatakan begitu untuk semua loop sampai C++26 keluar, tetapi seperti yang ditunjukkan, C sendiri tidak demikian; itu hanya berlaku ketika “ekspresi kontrolnya bukan ekspresi konstan”
      Karena itu, loop tak hingga sederhana di C seperti for (;;); seharusnya benar-benar dikompilasi sebagai loop tak hingga, dan loop {} milik Rust yang tidak terlalu opak juga seharusnya begitu
      Namun 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

  • 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

    • Secara keseluruhan rasanya seperti membandingkan apel dengan salad buah, tetapi cukup tepat untuk menunjukkan perbedaan penggunaan JS dan Zig
      Contoh Zig memakai array bertipe yang diketahui dengan ukuran tetap, sedangkan kode JS bersifat “generik” saat runtime, sehingga x dan y bisa berupa objek apa pun
      Di 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 65536 sama dengan panjang kedua array, sehingga ia memasukkan guard
      Meski begitu, dalam praktiknya tidak ada orang yang menulis loop for seperti itu; mereka akan menulis i < x.length, dan dalam kasus ini JIT setidaknya menghapus satu pemeriksaan array
    • Dalam contoh godbolt Rust dan Zig, target bisa diganti ke CPU yang lebih lama
      Maaf, 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

    • Saya setuju, dan lebih jauh lagi, menurut saya perbedaan mendasar antara bahasa tingkat tinggi dan bahasa tingkat rendah adalah bahwa dalam bahasa tingkat tinggi kita mengekspresikan intensi, sedangkan dalam bahasa tingkat rendah kita terpaksa mengekspresikan mekanisme underlying
    • Intensi yang dimaksud di sini tampaknya bukan “hitung tarif pajak pembelian ini”, melainkan lebih seperti “geser byte ini tiga posisi ke kiri”
      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 dihasilkan
  • Sintaks loop for itu mengerikan
    Apakah 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

    • Array seperti itu adalah pola yang sangat umum dalam kode tingkat rendah, apa pun bahasanya, begitu juga menelusurinya secara paralel
      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

    • Di Go pun allocator kustom dan arena memungkinkan dan memang ada, tetapi usability-nya sangat buruk dan sulit dipakai dengan benar
      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=native dan optimisasi seluruh program, serta mengompilasi semua kode Zig dalam proyek sebagai satu unit kompilasi
    Hampir semua “trik” seperti memakai unreachable sebagai petunjuk optimisasi juga bisa dilakukan di C, hanya saja kadang memerlukan ekstensi bahasa nonstandar
    Compiler 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

    • Terkait contoh casting, cast bisa dibungkus dalam fungsi
      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
    • Zig punya ide-ide menarik, dan saya kira tulisannya akan lebih berfokus pada optimisasi low-level, tetapi akhirnya isinya adalah “comptime dan kompilasi seluruh program itu hebat”
      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
    • Jika backend x86 baru keluar, mungkin kita bisa melihat bagian perbedaan performa antara C dan Zig yang memang dapat dikaitkan hanya dengan proyek Zig itu sendiri
    • Terkait casting bilangan bulat eksplisit, sepertinya akan segera ada perapian: https://ziggit.dev/t/short-math-notation-casting-clarity-of-...
    • Dalam beberapa tahun ke depan, tren penambahan fitur di seluruh bahasa tampaknya akan bergerak cukup kuat ke arah bahasa yang lebih verbose dan eksplisit
      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