rust & llvm
Latihan membuat instruksi pembelajaran dalam memahami keterkaitan pemrograman Rust dengan LLVM.
Memberikan pemahaman konsep dan praktik dasar tentang bagaimana Rust menggunakan LLVM (Low Level Virtual Machine) untuk melakukan optimisasi dan menghasilkan kode mesin. Anda akan belajar melihat antara tahapan kompilasi (LLVM IR, optimisasi, codegen → assembly), menggunakan LLVM tools sederhana, dan memahami dampak opsi build (debug vs release). Akhirnya Anda akan mengerti kenapa memahami LLVM berguna untuk debugging, optimisasi, dan interoperabilitas (FFI).
Tujuan Pembelajaran
Setelah menyelesaikan materi ini Anda dapat:
Menjelaskan peran LLVM dalam toolchain Rust (mengapa Rust “lower” ke LLVM Intermediate Representation/IR).
Meminta
rustcuntuk mengeluarkan LLVM IR dan assembly, lalu membaca bagian-bagian penting IR sederhana.Menjalankan beberapa LLVM tools dasar (
opt,llc) untuk melihat efek optimisasi dan menghasilkan assembly dari IR.Mengetahui perbedaan debug vs release terkait optimisasi dan observabilitas kode.
Mengenali situasi di mana pengetahuan LLVM membantu (profiling, debugging optimized code, FFI).
Menyebut beberapa bahasa lain yang memakai LLVM (Clang/C/C++, Swift, Julia, Kotlin/Native, dll.) dan memahami persamaan/perbedaannya.
Prasyarat
Rust toolchain terpasang (
rustup,cargo,rustc).Akses ke terminal (Linux/macOS/WSL sangat direkomendasikan).
(Opsional) Instalasi LLVM tools seperti
opt,llc,llvm-disjika ingin menjalankan langkah-langkah praktik, biasanya tersedia lewat paketllvmdi distro.
Pengenalan Singkat: Apa itu LLVM?
LLVM singkatan historis Low Level Virtual Machine, proyek open-source yang menyediakan infrastruktur kompilasi: intermediate representation (IR), optimizers, dan code generators untuk berbagai arsitektur CPU.
Banyak compiler modern menurunkan kode bahasa tingkat tinggi menjadi LLVM IR terlebih dahulu. LLVM kemudian melakukan optimisasi berbasis IR dan menghasilkan assembly untuk target spesifik.
Rust menggunakan LLVM sebagai backend: setelah Rust melakukan analisis bahasa-spesifik (ownership/borrow checking, type checking, MIR → Mid-level IR), Rust menurunkan kode menjadi LLVM IR agar LLVM melakukan optimisasi dan code generation.
Gambaran Alur Kompilasi Rust yang disederhanakan
Source (Rust) → parsing & macro expansion → AST (Abstract Syntax Tree)
AST → semantic checks (type checking, borrow checking)
Rust → MIR (Mid-level IR) → analisis/optimisasi khas Rust
MIR → LLVM IR → optimisasi LLVM (opt passes)
LLVM IR → assembly → assembler → object (.o)
Link object files + libraries → executable
Catatan: Anda akan fokus pada langkah 4–5: LLVM IR → assembly.
Langkah-Langkah Praktis
A. Siapkan project kecil
Buat project Rust sederhana:
cargo new llvm_demo
cd llvm_demoIsi src/main.rs:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let x = add(2, 3);
println!("sum = {}", x);
}Komentar: fungsi add sangat sederhana, bagus untuk melihat bagaimana fungsi tingkat tinggi menjadi instruksi rendah.
B. Emit LLVM IR dari rustc
Dua cara: (1) gunakan rustc langsung, (2) gunakan cargo rustc untuk opsi per-crate. Untuk pemula, jalankan rustc pada file:
# di dalam folder project, kompilasi file langsung untuk melihat IR
rustc --emit=llvm-ir src/main.rs -O -C debuginfo=0Perintah menghasilkan file src/main.ll (LLVM IR).
Catatan opsi:
-Oadalah shorthand untuk optimasi; tanpa-OIR akan lebih “straightforward”, tapi kurang dioptimalkan.-C debuginfo=0mematikan debug info di hasil IR agar lebih ringkas; Anda dapat menghilangkan itu kalau ingin debug info.
C. Baca contoh LLVM IR sederhana (fragmen)
Anda akan menemukan sesuatu yang mirip berikut (disederhanakan):
; ModuleID = 'main'
define i32 @add(i32 %0, i32 %1) {
entry:
%addtmp = add i32 %0, %1
ret i32 %addtmp
}
@.str = private unnamed_addr constant [8 x i8] c"sum = %d\0A\00", align 1
define i32 @main() {
entry:
%call = call i32 @add(i32 2, i32 3)
; ... prepare arguments, call printf-like...
ret i32 0
}Penjelasan singkat (pemula):
define i32 @add(i32 %0, i32 %1): mendeklarasikan sebuah fungsiaddyang menerima duai32dan mengembalikani32.%addtmp = add i32 %0, %1: operasi penjumlahan pada tingkat IR.ret i32 %addtmp: mengembalikan hasil.@.str: global constant, string format untukprintln!(Rust runtime memanggil fungsi librari C untuk printing di akhir).
D. Jalankan optimisasi sederhana dengan opt (opsional)
Jika Anda memasang LLVM tools, Anda bisa menjalankan optimizer opt untuk melihat efek pass tertentu:
# jalankan LLVM optimizer pass "instcombine" contoh
opt -instcombine main.ll -o main_opt.llopt dapat menjalankan banyak pass (inlining, constant propagation, loop unroll). Membaca hasil main_opt.ll akan menunjukkan perbedaan, mis. operasi sederhana bisa digabung.
E. Hasilkan assembly dengan llc (atau mint via rustc)
llc mengubah LLVM IR menjadi assembly untuk target tertentu:
llc -filetype=asm main.ll -o main.sLalu lihat main.s (assembly) untuk langkah lebih langsung ke instruksi mesin.
Atau langsung minta rustc menghasilkan assembly:
rustc --emit=asm src/main.rs -OLihat file .s.
F. Dari assembly ke object dan linking
Assembler (as) menghasilkan object (.o), lalu linker menggabungkan object menjadi executable. rustc biasanya menjalankan semua ini secara otomatis.
Contoh Konkret: interpretasi IR singkat
Ambil potongan:
define i32 @add(i32 %0, i32 %1) {
entry:
%addtmp = add i32 %0, %1
ret i32 %addtmp
}Interpretasi pemula:
define i32 @add(...)= "Saya mendefinisikan fungsi bernamaaddyang mengembalikan integer 32-bit".entry:= label awal blok (basic block) fungsi.%addtmp = add i32 %0, %1= buat variabel temporary dengan hasil penjumlahan argumen 0 dan 1.ret i32 %addtmp= kembalikan hasil itu ke pemanggil.
IR mirip “assembly tingkat tinggi” yang masih menyimpan struktur fungsi & nilai ter-typed.
Mengapa memahami LLVM berguna untuk programmer Rust?
Memahami optimisasi: kenapa compiler menghilangkan variabel atau meng-inlin-ing fungsi? Karena LLVM melakukan optimisasi; melihat IR membantu memahami transformasi.
Debugging optimized code: jika bug hanya muncul dalam
--release, IR/assembly membantu menelusuri masalah.FFI / ABI: saat menulis
extern "C"atau bekerja dengan crate native, memahami ABI & calling convention membantu menyelesaikan masalah linking.Instruksi khusus target: untuk target embedded atau vectorization, melihat assembly membantu memverifikasi penggunaan intrinsics/SIMD.
Perbandingan: Bahasa lain yang memakai LLVM
Clang (C/C++): Clang menurunkan C/C++ ke LLVM IR lalu manfaatkan LLVM. Bahasa ini sangat mirip prosesnya; perbedaan besar ada di front-end (parsing C/C++ vs parsing Rust semantics).
Swift: memakai LLVM untuk codegen; front-end fokus pada model memori Swift.
Kotlin/Native: dapat menggunakan LLVM untuk menghasilkan native binaries.
Julia: secara dinamis menurunkan ke LLVM IR pada run-time untuk JIT (Just-In-Time compilation) performance.
Haskell (GHC): memiliki backend LLVM opsional.
Perbedaan utama: front-end tiap bahasa berbeda (syntax, semantics, model memori), tapi semua mendapat manfaat optimisasi LLVM.
Edge Cases dan Hal Yang Sering Membingungkan Pemula
IR bukan "sumber aslinya": nama variabel asli biasanya hilang; hanya fungsi & global symbol yang terlihat (jika tidak di-strip).
Compiler versions berpengaruh: LLVM IR dan optimisasi berubah antar versi LLVM; IR yang dihasilkan dengan LLVM 9 bisa berbeda vs LLVM 12. Hal ini dapat mempengaruhi debugging reproducibility.
Optimisasi menyulitkan debugging: release builds menginline dan mengoptimalkan, membuat stepping & inspecting nilai sulit. Jika Anda butuh debug, gunakan build debug atau
releasedengandebug = true.Target-specific instruksi: assembly berbeda per arsitektur (x86_64 vs aarch64). Jangan harap assembly portabel.
Unsafe/UB:
unsafeRust dapat menyebabkan hasil IR/assembly yang tidak stabil jika UB (undefined behavior) terjadi. UB bisa membuat LLVM melakukan assumptions yang mengejutkan.Procedural macros: kode yang dihasilkan macro bisa membingungkan jejak asli; IR mencerminkan kode yang akhirnya dikompilasi setelah macro expansion.
Latihan Praktik Untuk Pemula
Catatan: semua perintah diasumsikan dijalankan di dalam folder proyek Rust yang sudah dibuat (mis.
llvm_demo) dan di terminal Linux/macOS/WSL. Jika Anda belum membuat proyek, jalankancargo new llvm_demolalu editsrc/main.rssesuai contoh sebelumnya.
Latihan A: Menghasilkan LLVM IR dan membaca fungsi add
Tujuan: melihat bentuk Low Level (LLVM IR) untuk fungsi Rust sederhana.
Langkah:
(1) Pastikan src/main.rs berisi fungsi sederhana, misalnya:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let s = add(2, 3);
println!("sum = {}", s);
}(2) Jalankan perintah berikut untuk meminta rustc menghasilkan file LLVM IR:
rustc --emit=llvm-ir src/main.rs -OOpsi
-Omeminta optimisasi; jika ingin IR yang lebih "mentah" tanpa optimisasi gunakan tanpa-O.
(3) Setelah perintah selesai, akan ada file bernama src/main.ll. Buka file itu:
less src/main.llatau
cat src/main.ll(4) Apa yang dicari (clue):
Cari baris yang mulai dengan
define i32 @add— itu adalah definisi fungsiadd.Di dalam blok fungsi, cari instruksi
add(mis.%addtmp = add i32 %0, %1) danret i32 %addtmp.
Contoh potongan IR yang mungkin Anda lihat:
define i32 @add(i32 %0, i32 %1) {
entry:
%addtmp = add i32 %0, %1
ret i32 %addtmp
}Penjelasan singkat:
define i32 @add(i32 %0, i32 %1)= ada fungsi bernamaaddyang menerima duai32dan mengembalikani32.add i32 %0, %1= operasi penjumlahan pada tingkat IR.ret i32 %addtmp= mengembalikan hasil penjumlahan.
Latihan B: Menghasilkan assembly dan membandingkan debug vs release
Tujuan: melihat perbedaan hasil kompilasi saat optimisasi aktif (release) vs tidak (debug), dan memahami dampaknya pada ukuran dan assembly.
Langkah:
(1) Tambahkan fungsi yang sedikit berat (loop) di src/main.rs:
fn sum(n: u64) -> u64 {
(0..n).fold(0, |a, b| a + b)
}
fn main() {
println!("{}", sum(1000));
}(2) Build debug (default):
cargo build(3) Build release (optimisasi):
cargo build --release(4) Bandingkan ukuran binary:
ls -lh target/debug/llvm_demo target/release/llvm_demoClue: perhatikan file
target/release/llvm_demobiasanya lebih kecil atau lebih cepat (ukuran bisa lebih kecil atau besar tergantung debug info), tapi kodenya dioptimalkan untuk performa.
(5) Disassemble kedua binary untuk melihat perbedaan assembly:
objdump -d target/debug/llvm_demo | less
objdump -d target/release/llvm_demo | lessJika
objdumpterlalu panjang, Anda bisa mencari bagian fungsisumdengangrepterhadap nama simbol (atau gunakan demangle):
nm -C target/release/llvm_demo | grep sumApa yang dicari (clue):
Di build debug Anda akan melihat loop yang tampak mirip dengan struktur Rust asli (instruksi untuk increment, compare, branch).
Di build release kemungkinan LLVM meng-optimalkan loop: bisa ada inlining (fungsi
sumdisisip langsung kemain), penghilangan variabel temporer, atau bahkan transformasi yang membuat loop menjadi lebih ringkas atau menggunakan instruksi vektor (vectorized) pada CPU tertentu.
Contoh hasil yang mungkin Anda lihat:
Debug assembly: terlihat banyak instruksi terkait pengelolaan loop (compare + branch), dan variabel masih jelas.
Release assembly: loop mungkin dipadatkan, ada sedikit pengulangan instruksi karena optimisasi; beberapa fungsi bisa di-inline, sehingga nama fungsi
sumkadang tidak muncul dengan jelas di assembly.
Latihan C: Menjalankan opt (gunakan jika LLVM tools terpasang)
Tujuan: melihat efek sebuah optimisasi LLVM pada LLVM IR.
Catatan: langkah ini memerlukan LLVM tools seperti opt dan llc. Jika Anda belum menginstalnya, lewati latihan ini atau pasang paket LLVM (sudo apt install llvm di beberapa distro).
Langkah:
(1) Setelah Anda punya src/main.ll (dari Latihan A), jalankan optimizer opt untuk melakukan satu pass optimisasi:
opt -S -instcombine src/main.ll -o src/main_opt.ll-Sberarti keluaran berupa file teks IR (bukan bitcode);-instcombineadalah nama pass optimisasi yang menyederhanakan instruksi aritmetika sederhana.
(2) Buka src/main.ll dan src/main_opt.ll untuk membandingkan:
diff -u src/main.ll src/main_opt.ll | lessApa yang dicari (clue):
Cari apakah operasi aritmetika sederhana dilipat menjadi nilai konstan di IR baru, atau apakah beberapa instruksi digabung.
Misalnya, jika original IR punya beberapa instruksi penjumlahan berantai, after
instcombineinstruksi itu mungkin disederhanakan menjadi satu operasi atau konstanta.
Contoh sebelum dan sesudah, ilustrasi sederhana:
Sebelum (main.ll):
%1 = add i32 %a, %b
%2 = add i32 %1, 0
ret i32 %2Sesudah (main_opt.ll) setelah instcombine:
%1 = add i32 %a, %b
ret i32 %1atau bahkan jika operands konstan:
; sebelum
%1 = add i32 2, 3
ret i32 %1
; sesudah
ret i32 5Penjelasan singkat: instcombine menggabungkan/menyederhanakan instruksi yang redundan sehingga IR menjadi lebih efisien sebelum proses codegen.
Jika Anda Tidak Memiliki LLVM tools (opt, llc)
Tidak apa-apa: Anda tetap bisa belajar banyak hanya dengan
rustc --emit=llvm-irdanrustc --emit=asm.Untuk melihat optimisasi sederhana, bandingkan
rustc --emit=llvm-ir src/main.rsdengan/ tanpa-O.Jika Anda ingin memasang LLVM: di Linux biasanya
sudo apt install llvmataubrew install llvmdi macOS. Setelah terpasang, periksa versi denganopt --versiondanllc --version.
Ringkasan Clue untuk Ketiga Latihan
Latihan A: Cari
define i32 @adddimain.ll. Lihat instruksiadddanret.Latihan B: Perhatikan ukuran file di
target/debugvstarget/release. Di release assembly sering lebih ringkas akibat optimisasi; fungsi bisa di-inline.Latihan C: Setelah menjalankan
opt -instcombine, cari pola instruksi yang dihapus atau digabung (mis. penjumlahan konstan diganti nilai konstan).
Tools yang berguna
rustc(Rust compiler),cargo(Rust package manager/build tool)LLVM tools:
opt(optimizer),llc(LLVM static compiler → assembly),llvm-dis(disassembler),llvm-as(assembler)Inspectors:
objdump,nm,readelf,stringsDebuggers:
rust-gdb,rust-lldb, VSCode + CodeLLDBDecompiler/analysis: Ghidra, radare2, IDA (konsepual)
Refleksi dengan clue singkat
Mengapa Rust menurunkan ke MIR dulu sebelum ke LLVM IR?
Clue: MIR memungkinkan pemeriksaan semantics bahasa Rust (borrow checker) dan optimisasi spesifik Rust sebelum LLVM melakukan optimisasi umum.
Jawaban singkat: untuk memisahkan concerns: analisis bahasa-spesifik di tingkat menengah, optimisasi & codegen generik di LLVM.Bagaimana optimisasi LLVM dapat "mengacaukan" debugging?
Clue: inlining & elimination dapat menghilangkan variabel & mengubah control flow.
Jawaban singkat: optimisasi mengubah struktur program jadi debugger tidak selalu bisa menunjuk ke baris source yang sama.Apa perbedaan praktis antara
rustc --emit=llvm-irdan hanyacargo build --release?
Clue:--emit=llvm-irmemberi Anda IR;cargo build --releasemenjalankan seluruh pipeline (IR → assembly → link) dengan optimisasi release.
Jawaban singkat:--emituntuk inspeksi tahap internal;cargo builduntuk hasil akhir.Kapan pengetahuan tentang LLVM berguna sehari-hari?
Clue: debugging bugs di release, menulis FFI, men-debug performa, menulis unsafe code.
Jawaban singkat: saat butuh mengerti transformasi compiler atau menyelidiki masalah performa/ABI.
Referensi (resmi / suggested reading)
The Rust Programming Language (The Rust Book). Bab-bab Ownership, Compilation, and FFI. — https://doc.rust-lang.org/book/
The rustc book (rustc internals) — https://doc.rust-lang.org/rustc/
LLVM Project Documentation — https://llvm.org/docs/
LLVM Language Reference Manual (IR) — https://llvm.org/docs/LangRef.html
Rust
rustcmanual:rustc --helpandrustc --print target-listfor target info.LLVM tools manual:
opt --help,llc --help,llvm-dis --help.


