Setiap kali kita menjalankan cargo build atau cargo run tanpa argumen tambahan, Cargo memberikan kita biner profil dev alias debug: cepat dikompilasi, mudah di-debug, tetapi sama sekali tidak dioptimasi. Biner jenis ini memang pas untuk siklus koding harian, namun menjadi masalah besar bila dikirim ke server produksi karena kecepatannya bisa jauh lebih lambat daripada seharusnya. Cargo menyediakan profil release yang mengaktifkan optimasi penuh LLVM lewat perintah cargo build --release. Pada chapter ini kita akan mengukur perbedaan keduanya dengan program penghitung bilangan prima yang bekerja keras, membedah flag kompilasi yang berbeda di balik layar, lalu melihat satu perubahan perilaku yang sering mengejutkan pemula: overflow check yang aktif di profil dev tetapi mati di profil release.

Dua Profil Bawaan Cargo #

Cargo membawa dua profil siap pakai yang menentukan bagaimana rustc mengompilasi proyek kita. Profil dipilih lewat flag pada perintah build atau run, dan hasilnya diletakkan di folder terpisah: target/debug untuk profil dev dan target/release untuk profil release. Tabel berikut merangkum perbedaan intinya:

AspekProfil devProfil release
Optimasi (opt-level)0 (tanpa optimasi)3 (optimasi penuh)
Debug infolengkap (-C debuginfo=2)dibuang bawaan sejak Cargo 1.77
Overflow checkaktif, panic saat terjadinonaktif, nilai membungkus
debug_assertions!aktifnonaktif
Target foldertarget/debugtarget/release

Profil dev sengaja tidak dioptimasi karena optimasi memakan waktu kompilasi dan membuat pemetaan antara baris kode dengan instruksi mesin menjadi kacau. Profil release membalik semua prioritas tersebut: kompilasi lebih lambat, tetapi biner berjalan secepat mungkin.

Mengukur Perbedaan dengan Program Nyata #

Kita siapkan proyek uji bernama penghitung-prima yang menghitung jumlah bilangan prima sampai satu juta dengan pencobaan pembagian. Beban kerja murni aritmetika ini memberi LLVM ruang optimasi yang lebar sehingga perbedaan kedua profil terlihat jelas.

rust
use std::time::Instant;

fn hitung_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
}

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

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

Mari kita bedah peran baris-baris pentingnya:

  1. Baris 1 (use std::time::Instant;): mengimpor tipe stopwatch dari pustaka standar. Instant membaca jam monotonik sehingga pengukuran tidak terpengaruh perubahan jam dinding.
  2. Baris 3 (fn hitung_prima(batas: u64) -> u64): fungsi penghitung menerima batas atas dan mengembalikan banyaknya bilangan prima. Seluruh hot loop program berada di dalam fungsi ini.
  3. Baris 8 (while pembagi * pembagi <= n): loop pembagi yang berjalan sampai akar kuadrat n. Loop bersarang for-while inilah yang memakan sebagian besar waktu eksekusi.
  4. Baris 23 (let mulai = Instant::now();): mengambil titik waktu sebelum perhitungan dimulai.
  5. Baris 25 (let durasi = mulai.elapsed();): menghitung selisih waktu sejak mulai. Nilainya bertipe Duration yang dicetak dengan format {:?} pada baris 28.

Sekarang kita bangun kedua profil dan menjalankan masing-masing biner:

bash
cargo build
cargo build --release
./target/debug/penghitung-prima
./target/release/penghitung-prima

Output yang Dihasilkan:

text
Jumlah prima sampai 1000000: 78498
Waktu eksekusi: 272.813621ms
Jumlah prima sampai 1000000: 78498
Waktu eksekusi: 117.376707ms

Pada mesin tempat chapter ini diuji, biner dev membutuhkan sekitar 273 milidetik sedangkan biner release sekitar 117 milidetik — program yang sama berjalan sekitar 2,3 kali lebih cepat. Angka persisnya pasti berbeda di mesin Anda karena bergantung pada prosesor, beban sistem, dan versi compiler, namun pola besarnya konsisten: release jauh lebih cepat. Selisih pada program lain bisa lebih dramatis — kode yang sarat pemanggilan fungsi kecil dan akses koleksi bisa melompat 10 kali lebih cepat atau lebih.

Ukuran biner juga berbeda drastis:

bash
ls -lh target/debug/penghitung-prima target/release/penghitung-prima

Output yang Dihasilkan:

text
-rwxrwxr-x 2 z z 4.4M Oct 13 23:03 target/debug/penghitung-prima
-rwxrwxr-x 2 z z 444K Oct 13 23:03 target/release/penghitung-prima

Biner dev 4,4 MB sedangkan biner release hanya 444 KB. Angka tanggal dan ukuran persis bervariasi per mesin; keduanya diambil dari satu eksekusi nyata.

Flag yang Berbeda di Balik Layar #

Kita bisa mengintip flag apa saja yang dikirim Cargo kepada rustc dengan menambahkan -v pada perintah build. Pada profil release, potongan pemanggilan rustc memuat dua flag kunci:

text
rustc --crate-name penghitung_prima ... -C opt-level=3 ... -C strip=debuginfo ...

Sementara pada profil dev, flag yang menonjol adalah:

text
rustc --crate-name penghitung_prima ... -C debuginfo=2 ...

Flag opt-level=0 di profil dev memberi tahu LLVM untuk tidak melakukan transformasi apa pun pada kode: setiap statement diterjemahkan menjadi instruksi mesin yang lurus dan setiap pemanggilan fungsi tetap menjadi pemanggilan sungguhan. Profil release memakai opt-level=3, yang mengizinkan LLVM melakukan inlining (menempelkan isi fungsi ke pemanggilnya sehingga biaya pemanggilan hilang), penggabungan loop, eliminasi kode mati, dan vektorisasi SIMD. Optimasi inilah sumber lompatan kecepatan yang kita ukur.

Perbedaan ukuran biner datang dari dua flag lain. Flag -C debuginfo=2 pada profil dev menyisipkan informasi debug DWARF — pemetaan antara instruksi mesin dan baris sumber — ke dalam biner. Kita bisa membuktikannya dengan menghitung section .debug_* menggunakan objdump -h: biner dev memuat 8 section debug, sedangkan biner release memuat 0 karena Cargo sejak versi 1.77 secara bawaan menjalankan strip=debuginfo pada profil release. Sisa ukuran 444 KB itu kini diisi symbol table dan mesin panic yang akan kita pangkas pada chapter Strip dan Ukuran Biner.

Perilaku Overflow yang Berbeda #

Profil dev mengaktifkan debug-assertions dan overflow check aritmetika; profil release menonaktifkan keduanya demi kecepatan. Program berikut menambahkan nilai stok melewati batas u32:

rust
fn main() {
    let mut stok: u32 = u32::MAX - 5;
    for _ in 0..10 {
        stok += 1;
    }
    println!("Stok sekarang: {}", stok);
}

Output yang Dihasilkan (profil dev):

text
thread 'main' (25471) panicked at src/main.rs:4:9:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Output yang Dihasilkan (profil release):

text
Stok sekarang: 4

Profil dev menghentikan program dengan panic attempt to add with overflow dan exit code 101; angka dalam tanda kurung adalah ID thread yang berbeda pada setiap eksekusi. Profil release membiarkan penjumlahan membungkus ke nilai 4 tanpa keluhan apa pun karena itulah perilaku pembungkusan dua komplemen yang murah di level prosesor. Perbedaan ini bukan bug melainkan pilihan desain: dev menangkap kesalahan logika sedini mungkin, release mempercayai bahwa overflow sudah diperiksa saat pengembangan.

Ringkasan #

Perintah cargo build --release memindahkan hasil kompilasi ke target/release dengan opt-level=3, tanpa debug info, dan tanpa overflow check. Pada pengukuran kita, beban kerja prima satu juta berjalan sekitar 273 milidetik di dev dan 117 milidetik di release, dengan ukuran biner menyusut dari 4,4 MB menjadi 444 KB — angka spesifik yang akan berbeda di mesin lain namun arahnya selalu sama. Profil dev tetap default yang tepat untuk koding harian karena kompilasinya cepat dan error logika tertangkap lebih dini. Pertanyaan berikutnya setelah flag opt-level=3 menyala: apakah optimasi LLVM benar-benar menembus batas antar crate dependensi kita? Jawabannya ada pada chapter LTO dan Codegen Units.