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
rustc
untuk 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-dis
jika ingin menjalankan langkah-langkah praktik, biasanya tersedia lewat paketllvm
di 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_demo
Isi 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=0
Perintah menghasilkan file src/main.ll
(LLVM IR).
Catatan opsi:
-O
adalah shorthand untuk optimasi; tanpa-O
IR akan lebih “straightforward”, tapi kurang dioptimalkan.-C debuginfo=0
mematikan 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 fungsiadd
yang menerima duai32
dan 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.ll
opt
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.s
Lalu lihat main.s
(assembly) untuk langkah lebih langsung ke instruksi mesin.
Atau langsung minta rustc
menghasilkan assembly:
rustc --emit=asm src/main.rs -O
Lihat 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 bernamaadd
yang 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
release
dengandebug = true
.Target-specific instruksi: assembly berbeda per arsitektur (x86_64 vs aarch64). Jangan harap assembly portabel.
Unsafe/UB:
unsafe
Rust 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_demo
lalu editsrc/main.rs
sesuai 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 -O
Opsi
-O
meminta 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.ll
atau
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 bernamaadd
yang menerima duai32
dan 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_demo
Clue: perhatikan file
target/release/llvm_demo
biasanya 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 | less
Jika
objdump
terlalu panjang, Anda bisa mencari bagian fungsisum
dengangrep
terhadap nama simbol (atau gunakan demangle):
nm -C target/release/llvm_demo | grep sum
Apa 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
sum
disisip 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
sum
kadang 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
-S
berarti keluaran berupa file teks IR (bukan bitcode);-instcombine
adalah 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 | less
Apa 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
instcombine
instruksi 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 %2
Sesudah (main_opt.ll
) setelah instcombine:
%1 = add i32 %a, %b
ret i32 %1
atau bahkan jika operands konstan:
; sebelum
%1 = add i32 2, 3
ret i32 %1
; sesudah
ret i32 5
Penjelasan 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-ir
danrustc --emit=asm
.Untuk melihat optimisasi sederhana, bandingkan
rustc --emit=llvm-ir src/main.rs
dengan/ tanpa-O
.Jika Anda ingin memasang LLVM: di Linux biasanya
sudo apt install llvm
ataubrew install llvm
di macOS. Setelah terpasang, periksa versi denganopt --version
danllc --version
.
Ringkasan Clue untuk Ketiga Latihan
Latihan A: Cari
define i32 @add
dimain.ll
. Lihat instruksiadd
danret
.Latihan B: Perhatikan ukuran file di
target/debug
vstarget/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
,strings
Debuggers:
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-ir
dan hanyacargo build --release
?
Clue:--emit=llvm-ir
memberi Anda IR;cargo build --release
menjalankan seluruh pipeline (IR → assembly → link) dengan optimisasi release.
Jawaban singkat:--emit
untuk inspeksi tahap internal;cargo build
untuk 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
rustc
manual:rustc --help
andrustc --print target-list
for target info.LLVM tools manual:
opt --help
,llc --help
,llvm-dis --help
.