Compiler C++ modern jauh lebih rajin daripada sekadar menerjemahkan kode baris demi baris. Dengan flag optimasi seperti -O2, compiler menulis ulang program kita secara internal: fungsi kecil dilekatkan ke pemanggilnya, perhitungan konstanta digeser ke compile time, kode yang tidak berefek dibuang, dan loop disusun ulang agar memori terbaca berurutan. Bagian pentingnya: semua penulisan ulang ini harus mempertahankan perilaku program yang teramati, sebuah kontrak yang disebut as-if rule. Chapter ini membongkar apa yang dikerjakan optimizer, apa yang tidak bisa dikerjakannya, dan bagaimana kita membantunya dengan constexpr.

Apa yang Dilakukan Compiler Optimizer #

Empat kategori optimasi paling sering bekerja di belakang layar:

  • Inlining: badan fungsi kecil ditempel langsung di pemanggilnya, sehingga biaya pemanggilan fungsi hilang dan optimasi lintas batas fungsi bisa berlanjut.
  • Constant folding dan propagation: ekspresi yang seluruh operandnya konstan (misalnya 3600 * 24) dihitung saat kompilasi, dan nilai yang dikenal disebar ke tempat pemakaiannya.
  • Dead code elimination: instruksi yang hasilnya tidak pernah dibaca atau tidak berefek teramati dihapus tanpa ampun.
  • Loop optimization: loop di-unroll, perhitungan yang tidak berubah di dalam loop digeser keluar (loop-invariant motion), dan akses memori ditata ulang agar ramah cache.

Poin kritisnya: optimizer bekerja pada level instruksi dan konstanta, bukan pada tingkat algoritma. Compiler tidak akan mengganti pencarian linear Anda dengan binary search, dan loop bersarang O(n²) tetap O(n²) secepat apa pun flag yang dipakai. Kenaikan kecepatan tipikal antara -O0 dan -O2 berkisar dua sampai lima kali, bukan ribuan kali — lompatan besar tetap datang dari pilihan algoritma seperti yang dibahas di awal section.

Hasil Sama, Kecepatan Berbeda #

Program berikut sengaja memuat tiga bahan optimasi: fungsi kecil kuadrat (kandidat inlining), kode yang hasilnya tidak pernah dibaca (kandidat dead code elimination), dan loop perhitungan biasa. Kontrak as-if bisa kita uji langsung: kompilasi di dua level, lalu bandingkan outputnya:

cpp
// Program contoh untuk melihat efek compiler optimizer.
// Perhatikan: hasil (output) program HARUS sama di -O0 maupun -O2 —
// optimizer mempercepat jalannya program, bukan mengubah hasilnya.
#include <iomanip>
#include <iostream>
#include <vector>

// Fungsi kecil yang pada -O2 biasanya di-inline ke pemanggilnya.
int kuadrat(int x) {
    return x * x;
}

double rataRataDiAtasAmbang(const std::vector<int>& nilai, int ambang,
                            int& terhitung) {
    double total = 0.0;
    terhitung = 0;
    for (int x : nilai) {
        if (x >= ambang) {
            total += x;
            ++terhitung;
        }
    }
    return total / terhitung;
}

int main() {
    std::vector<int> nilai;
    for (int i = 0; i <= 100; i += 5) nilai.push_back(i);

    int terhitung = 0;
    double rata = rataRataDiAtasAmbang(nilai, 60, terhitung);
    std::cout << std::fixed << std::setprecision(2)
              << "rata-rata >= 60 : " << rata << '\n';
    std::cout << "banyak elemen   : " << terhitung << '\n';
    std::cout << "kuadrat(12)     : " << kuadrat(12) << '\n';

    // Dead code: hasil penugasan ini tidak pernah dibaca,
    // sehingga optimizer pada -O2 boleh membuangnya.
    int pemboros = 0;
    pemboros = kuadrat(9999) * 3;
    (void)pemboros;
    std::cout << "program tetap berjalan sama dengan atau tanpa baris pemboros\n";
    return 0;
}

Output yang Dihasilkan (diuji pada -O0 — dan di bawah kita buktikan -O2 menghasilkan baris yang sama):

text
rata-rata >= 60 : 80.00
banyak elemen   : 9
kuadrat(12)     : 144
program tetap berjalan sama dengan atau tanpa baris pemboros

Bedah baris penting:

  1. kuadrat: badannya satu ekspresi, sehingga pada -O2 compiler hampir pasti melekatkannya ke pemanggil; tidak ada pemanggilan fungsi yang benar-benar terjadi saat program berjalan.
  2. rataRataDiAtasAmbang: menerima const std::vector<int>& — referensi konstan menghindari penyalinan seluruh vektor, sebuah keputusan yang TIDAK bisa diperbaiki compiler karena mengubah semantik kepemilikan.
  3. pemboros: hasil kuadrat(9999) * 3 tidak pernah dibaca; (void)pemboros hanya meredam warning. Pada -O2 seluruh penugasan ini dihapus dari binary — dan program tetap benar.
  4. Angka 80.00 dan 9: hasil komputasi identik di kedua level optimasi, bukti bahwa optimizer mengubah kecepatan, bukan makna.

Sesi berikut membandingkan kedua binary secara nyata di platform uji (Debian, g++ 14.2):

bash
g++ -std=c++23 -Wall -Wextra -O0 optimasi-compiler.cpp -o opt-O0
g++ -std=c++23 -Wall -Wextra -O2 optimasi-compiler.cpp -o opt-O2
./opt-O0
./opt-O2
ls -l opt-O0 opt-O2

Hasil Eksekusi (platform uji; ukuran byte bisa berbeda di sistem Anda, kesamaan output tidak):

text
== O0 ==
rata-rata >= 60 : 80.00
banyak elemen   : 9
kuadrat(12)     : 144
program tetap berjalan sama dengan atau tanpa baris pemboros
== O2 ==
rata-rata >= 60 : 80.00
banyak elemen   : 9
kuadrat(12)     : 144
program tetap berjalan sama dengan atau tanpa baris pemboros
opt-O0 32680
opt-O2 17256

Output kedua program identik byte-per-byte, sementara binary -O2 sekitar setengah ukuran -O0 untuk program sekecil ini — simbol debug dan kode tanpa optimasi membebani -O0.

Kode Tetap Harus Benar #

Karena optimizer hanya boleh mengubah bentuk, bukan makna, seluruh tanggung jawab kebenaran tetap di tangan kita. Bug buffer overflow, iterator invalidation, atau race condition tidak hilang dengan -O3; yang terjadi justru sebaliknya — build teroptimasi menyusun ulang kode sehingga lokasi crash bisa bergeser dari baris sumber aslinya, membuat debugging lebih menantang. Itulah alasan praktik standar: develop dan debug di -O0 atau -Og yang setia pada baris sumber, rilis di -O2.

Kesalahpahaman yang juga sering muncul: “compiler pasti sudah mengoptimasi semuanya, jadi saya tidak perlu peduli”. Bacaan yang benar adalah “compiler mengoptimasi bentuk kode yang saya tulis, pada algoritma yang saya pilih”. Penyalinan vektor satu juta elemen di dalam loop tetap mahal meski dikompilasi -O3, karena biayanya ada di keputusan desain, bukan di instruksi.

constexpr sebagai Optimizer Manual #

Ada pekerjaan yang bisa kita geser keluar dari runtime secara eksplisit: fungsi constexpr boleh dievaluasi compiler saat kompilasi bila seluruh argumennya konstanta. Ini optimasi yang kita minta langsung, bukan yang ditawar compiler:

cpp
// constexpr: "optimizer manual" — kita memaksa pekerjaan pindah
// dari runtime ke compile time. Hasilnya sama persis, biayanya nol
// saat program berjalan.
#include <iostream>

constexpr long long kuadrat(long long x) {
    return x * x;
}

int main() {
    // Konstanta compile time: compiler menghitungnya saat kompilasi.
    constexpr long long POPULASI_MAKS = kuadrat(2100);
    static_assert(POPULASI_MAKS == 4410000, "hitung compile time harus pas");

    // Ukuran array dari hasil constexpr — mustahil tanpa nilai compile time.
    int buffer[kuadrat(4)] = {};
    std::cout << "POPULASI_MAKS   = " << POPULASI_MAKS << '\n';
    std::cout << "sizeof(buffer)  = " << sizeof(buffer) << " byte\n";

    // Pemanggilan dengan nilai runtime tetap boleh; di situ berlaku
    // seperti fungsi biasa.
    long long masukan = 12345;
    std::cout << "kuadrat runtime = " << kuadrat(masukan) << '\n';
    return 0;
}

Output yang Dihasilkan:

text
POPULASI_MAKS   = 4410000
sizeof(buffer)  = 64 byte
kuadrat runtime = 152399025

Bedah baris penting:

  1. static_assert(POPULASI_MAKS == 4410000): asersi ini diverifikasi compiler saat kompilasi; program bahkan tidak akan selesai dikompilasi bila kuadrat(2100) tidak dihitung pada compile time.
  2. int buffer[kuadrat(4)]: ukuran array wajib konstanta compile time, jadi baris ini membuktikan evaluasi constexpr benar-benar terjadi — sizeof(buffer) = 64 byte (4 elemen × sizeof(int)).
  3. kuadrat(masukan) dengan variabel runtime: constexpr tetap bisa dipanggil seperti fungsi biasa; pemeriksaan “apakah ini compile time” dilakukan compiler per pemanggilan.

constexpr paling menguntungkan untuk tabel lookup, konstanta fisika, dan perhitungan ukuran — beban yang sekali dihitung saat kompilasi, gratis selamanya saat program berjalan. Untuk sisa pekerjaan runtime, pilihan kita tinggal mengatur seberapa keras compiler bekerja; urutan level optimasi -O0 sampai -Ofast menjadi topik chapter berikutnya.

Rujukan resmi: constexpr evaluation dan GCC Optimize Options .