Biner release dari chapter sebelumnya sudah menerima optimasi LLVM setinggi opt-level=3, tetapi ada batas yang jarang disadari pemula: optimasi tersebut berhenti di perbatasan crate. Setiap dependensi dikompilasi terpisah menjadi unit kode sendiri, sehingga LLVM tidak pernah melihat kode program kita dan kode pustaka dalam satu pandangan yang sama. Akibatnya, pemanggilan fungsi lintas crate tidak bisa di-inline, dan peluang optimasi di perbatasan itu menguap. Dua pengaturan profil release tingkat lanjut — LTO (Link Time Optimization) dan codegen-units — membuka perbatasan tersebut. Pada chapter ini kita akan membangun workspace dua crate, mengukur apa yang berubah ketika LTO dan codegen units disetel, dan jujur melihat kasus ketika keduanya mempercepat kompilasi bukan eksekusi.

Batas Optimasi di Perbatasan Crate #

Saat cargo build berjalan, setiap crate dikompilasi satu per satu menjadi berkas rlib yang berisi kode mesin jadi. Biner akhir lalu dirangkai dari kumpulan rlib tersebut pada tahap linking. Karena kode mesin dependensi sudah beku sebelum program utama dikompilasi, LLVM tidak bisa menempelkan isi fungsi milik dependensi ke pemanggilnya — teknik inlining lintas crate tertutup, kecuali fungsi itu diberi anotasi #[inline] atau berupa fungsi generik yang di-monomorphisasi di crate pemakai.

Kita bisa melihat bukti perilaku ini pada keluaran cargo build --release -v: pemanggilan rustc memuat flag -C embed-bitcode=no. Flag tersebut memberi tahu rustc bahwa bitcode LLVM (representasi menengah yang masih bisa dioptimasi ulang) tidak perlu disisipkan ke rlib, karena memang tidak akan ada optimasi lanjutan di tahap linking. LTO membalik keputusan itu: rlib disertai bitcode, dan pada saat linking seluruh crate dioptimal ulang menjadi satu kesatuan besar sebelum benar-benar menjadi kode mesin.

Codegen Units: Paralelisasi dengan Biaya #

Sebelum bicara LTO, kita perlu paham satuan kerja rustc bernama codegen unit (CGU). Untuk memanfaatkan banyak inti prosesor, rustc memecah satu crate menjadi beberapa CGU yang dikompilasi secara paralel. Profil release bawaan memakai 16 unit. Pemecahan ini mempercepat build, tetapi ada biayanya: fungsi yang berada di unit berbeda tidak bisa saling di-inline, dan analisis menyeluruh LLVM terpotong-potong.

Menyetel codegen-units = 1 memaksa seluruh crate dikompilasi sebagai satu unit tunggal. Dengan satu unit, rustc dan LLVM melihat kode crate secara utuh sehingga inlining dan propagasi konstanta menembus seluruh modul. Efek sampingnya jelas: paralelisasi hilang dan waktu kompilasi naik. Karena itu pengaturan ini hanya masuk akal untuk build artifact akhir, bukan untuk siklus koding yang menuntut build cepat.

Proyek Uji: Dua Crate dalam Satu Workspace #

Agar perbatasan crate benar-benar ada, kita siapkan workspace dengan crate pustaka hitung dan crate biner aplikasi yang memanggilnya. Struktur foldernya:

text
lto-demo/
├── Cargo.toml
├── crates/
│   └── hitung/
│       ├── Cargo.toml
│       └── src/lib.rs
└── aplikasi/
    ├── Cargo.toml
    └── src/main.rs

Tiga berkas Cargo.toml dibutuhkan. Root workspace menyimpan daftar anggotanya:

toml
[workspace]
resolver = "2"
members = ["crates/hitung", "aplikasi"]

Crate pustaka memakai manifes minimal:

toml
[package]
name = "hitung"
version = "0.1.0"
edition = "2021"

Crate biner mendeklarasikan dependensi path menuju crate pustaka:

toml
[package]
name = "aplikasi"
version = "0.1.0"
edition = "2021"

[dependencies]
hitung = { path = "../crates/hitung" }

Mari kita bedah berkas root Cargo.toml di atas:

  1. Baris 1 ([workspace]): menandai bahwa folder ini adalah root workspace yang mengoordinasikan beberapa crate sekaligus.
  2. Baris 2 (resolver = "2"): memilih resolver dependensi versi 2, perilaku baku untuk edition 2021 yang menghindari penyatuan fitur antar platform.
  3. Baris 3 (members = [...]): mendaftarkan dua anggota workspace: pustaka hitung dan biner aplikasi.

Crate pustaka memuat fungsi penghitung prima dari chapter sebelumnya, sementara crate biner memanggilnya di dalam pengukuran waktu:

rust
// crates/hitung/src/lib.rs
/// Menghitung jumlah bilangan prima sampai `batas` dengan pencobaan pembagian.
pub fn jumlah_prima(batas: u64) -> u64 {
    let mut jumlah = 0;
    for n in 2..=batas {
        let mut prima = true;
        let mut pembagi = 2;
        while pembagi * pembagi <= n {
            if n % pembagi == 0 {
                prima = false;
                break;
            }
            pembagi += 1;
        }
        if prima {
            jumlah += 1;
        }
    }
    jumlah
}
rust
// aplikasi/src/main.rs
use std::hint::black_box;
use std::time::Instant;

fn main() {
    let mulai = Instant::now();
    let jumlah = black_box(hitung::jumlah_prima(1_000_000));
    let durasi = mulai.elapsed();

    println!("Jumlah prima sampai 1000000: {}", jumlah);
    println!("Waktu eksekusi: {:?}", durasi);
}

Pemanggilan black_box menghalangi LLVM menyingkirkan hasil komputasi, trik yang akan kita jelaskan tuntas pada chapter Benchmark Sederhana. Dengan profil release bawaan (LTO mati, 16 codegen units), hasilnya di mesin uji kami:

text
Jumlah prima sampai 1000000: 78498
Waktu eksekusi: 116.695398ms

Biner berukuran 444 KB dan seluruh build selesai dalam 0,13 detik.

Gotcha: Profil Hanya Dibaca di Root Workspace #

Pemula sering menempelkan [profile.release] di Cargo.toml-nya crate aplikasi, lalu bingung karena pengaturannya tidak berefek. Cargo menolaknya dengan warning tegas:

text
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package:   /home/user/lto-demo/aplikasi/Cargo.toml
workspace: /home/user/lto-demo/Cargo.toml

Pesan ini berasal dari eksekusi nyata di mesin uji; hanya lokasi folder yang kami samarkan. Aturannya sederhana: dalam workspace, [profile.release] hanya diakui bila ditulis di Cargo.toml root. Pada proyek tunggal tanpa workspace, Cargo.toml proyek sekaligus menjadi root sehingga tidak ada masalah.

Mengaktifkan LTO dan Codegen Units #

Kita tambahkan pengaturan berikut ke Cargo.toml root, lalu bangun ulang:

toml
[profile.release]
lto = true
codegen-units = 1
  1. Baris 1 ([profile.release]): tabel konfigurasi profil release; seluruh pengaturan di bawahnya menimpa bawaan.
  2. Baris 2 (lto = true): mengaktifkan LTO penuh (fat LTO). Semua rlib membawa bitcode dan dioptimasi ulang menjadi satu kesatuan saat linking.
  3. Baris 3 (codegen-units = 1): memaksa setiap crate dikompilasi sebagai satu unit kode sehingga inlining menembus seluruh modul.

Hasil pengukuran ulang di mesin yang sama:

text
Jumlah prima sampai 1000000: 78498
Waktu eksekusi: 116.543793ms

Mari kita rangkum ketiga angka yang berubah — waktu build, ukuran biner, dan waktu eksekusi:

PengaturanWaktu buildUkuran binerWaktu eksekusi
Bawaan (LTO mati, 16 CGU)0,13 detik444 KB116,695 ms
lto = "thin"2,46 detik414 KB116,458 ms
lto = true + 1 CGU2,52 detik366 KB116,544 ms

Dua temuan jujur dari tabel ini. Pertama, waktu eksekusi program kita praktis tidak berubah — beban kerja penghitung prima terikat pada kecepatan instruksi pembagian prosesor, bukan pada pemanggilan fungsi lintas crate, sehingga inlining tambahan tidak punya ruang bermain. LTO bukan tombol ajaib yang mempercepat semua program; keuntungannya muncul pada kode yang sarat pemanggilan fungsi publik non-generik milik dependensi. Kedua, waktu build melonjak hampir 20 kali lipat (0,13 detik menjadi 2,52 detik) karena bitcode seluruh crate harus dioptimasi ulang secara menyatu dan paralelisasi CGU dimatikan. Sebagai gantinya, ukuran biner mengecil 17 persen menjadi 366 KB karena linker kini bisa membuang kode yang ternyata tidak terpakai lintas crate.

Fat, Thin, dan Kapan Dipakai #

Nilai lto punya tiga pilihan utama. Nilai false adalah bawaan: optimasi berhenti di perbatasan crate dan build tercepat. Nilai true sama dengan "fat": seluruh bitcode dilebur menjadi satu dan dioptimasi tuntas — paling lambat dibangun, hasil paling kompak. Nilai "thin" berada di tengah: crate tetap dipecah menjadi banyak gugus kecil yang dioptimasi paralel dengan koordinasi lintas gugus, sehingga hampir secepat fat dalam hasil tetapi lebih ramah waktu build; pada pengukuran kita thin menghasilkan 414 KB dengan 2,46 detik build.

Ringkasan #

LTO membuka perbatasan antar crate dengan membawa bitcode LLVM ke tahap linking, sedangkan codegen-units = 1 menutup pemecahan paralel di dalam crate agar inlining menyapu seluruh modul. Pengukuran kami menunjukkan sisi yang jarang diceritakan: pada beban kerja pembagian integer, waktu eksekusi tidak berubah, waktu build membesar hampir 20 kali lipat, dan ukuran biner mengecil 17 persen. Pengaturan ini layak dinyalakan untuk artifact akhir dan tidak untuk iterasi harian, serta wajib ditulis di Cargo.toml root workspace agar tidak diabaikan begitu saja. Biner 366 KB yang kita dapat masih mengandung symbol table dan mesin unwinding yang bisa dipangkas lebih jauh — lanjut ke chapter Strip dan Ukuran Biner.