Satu kode sumber bisa menghasilkan banyak program berbeda kecepatannya, cukup dengan mengganti flag optimasi. GCC menyediakan jenjang -O0 sampai -O3, plus dua varian khusus: -Os untuk ukuran dan -Ofast untuk kecepatan tanpa kompromi. Memilih level ini adalah keputusan build, bukan keputusan kode — kode sumbernya sama, tetapi binary yang dihasilkan berbeda ukuran, berbeda kecepatan, bahkan bisa berbeda hasil aritmetika. Chapter ini mengukur langsung perbedaan tersebut di platform uji (Debian, g++ 14.2, CPU 2 core), sekaligus merangkum kebiasaan Debug versus Release yang sudah kita singgung sejak chapter build.

Enam Level dalam Satu Tabel #

FlagRingkasanKapan dipakai
-O0tanpa optimasi, setia pada baris sumberdebugging harian
-Ogoptimasi ringan yang tidak mengganggu debuggingsiklus edit–compile–debug
-O1optimasi dasar tanpa trade-off ruang-waktubuild cepat untuk pengujian
-O2optimasi standar: inlining, vectorization hemat, dsb.default build rilis
-O3lebih agresif (unrolling lebih lebar, loop lebih dalam)hot path terukur, ukuran lebih besar
-Osoptimasi dengan fokus ukuran binaryperangkat kecil, cache instruksi ketat
-Ofast-O3 + -ffast-math + pelonggaran standarhanya bila keakuratan float boleh longgar

Dua baris terakhir perlu kewaspadaan ekstra. -O3 masih setia pada standar C++ — hasilnya sama dengan -O0, hanya lebih cepat dan biasanya lebih besar. -Ofast berbeda kelas: ia mengaktifkan -ffast-math, yang memberi izin pada compiler untuk mengatur ulang operasi floating-point, menganggap tidak ada NaN/Infinity, dan membaca standar matematika secara longgar. Izin itu berarti penjualan keakuratan, dan kita akan melihat buktinya sebentar lagi.

Mengukur Dampaknya di Platform Ini #

Program berikut berisi dua penjumlahan jutaan kali dan satu assert; keduanya akan merespons flag yang kita pilih:

cpp
// Dua perilaku yang bergantung pada flag kompilasi:
// 1. penjumlahan float sensitif terhadap pengaturan ulang operasi (fast-math)
// 2. assert hilang bila NDEBUG didefinisikan
#include <cassert>
#include <iomanip>
#include <iostream>

int main() {
    // 0.25f dapat diwakili persis di binary float, jadi penjumlahannya
    // eksak di level optimasi mana pun.
    float eksak = 0.0f;
    for (int i = 0; i < 10000000; ++i) eksak += 0.25f;

    // 0.1f TIDAK dapat diwakili persis; hasil akhirnya bergantung pada
    // urutan penjumlahan, dan fast-math mengizinkan compiler mengubah urutan.
    float kira = 0.0f;
    for (int i = 0; i < 10000000; ++i) kira += 0.1f;

    std::cout << std::setprecision(12);
    std::cout << "jumlah 0.25f : " << eksak << '\n';
    std::cout << "jumlah 0.1f  : " << kira << '\n';

    // assert hanya bekerja di build yang tidak mendefinisikan NDEBUG.
    int stok = 5;
    assert(stok > 0 && "stok harus positif");
#ifdef NDEBUG
    std::cout << "build release: assert dimatikan compiler\n";
#else
    std::cout << "build debug  : assert aktif, memeriksa stok > 0\n";
#endif
    return 0;
}

Output yang Dihasilkan (kompilasi -O0, hasil yang sama juga di -O1/-O2/-O3/-Os):

text
jumlah 0.25f : 2500000
jumlah 0.1f  : 1087937
build debug  : assert aktif, memeriksa stok > 0

Bedah baris penting:

  1. eksak dengan 0.25f: 0.25 dapat diwakili persis dalam binary float, sehingga hasilnya 2500000 bulat sempurna di semua level optimasi — operasi urutan mana pun memberi hasil sama.
  2. kira dengan 0.1f: 0.1 tidak terwakili persis, jadi tiap penambahan membawa galat pembulatan; hasil 1087937 adalah akumulasi galat urutan penjumlahan berurutan.
  3. assert(stok > 0): pada build tanpa NDEBUG, pemeriksaan benar-benar dieksekusi, dan cabang #ifdef melaporkan build debug.

Ukurannya? Kompilasi berkas yang sama di enam level menghasilkan binary berbeda (platform uji; angka byte milik program ini saja):

bash
for f in -O0 -O1 -O2 -O3 -Os -Ofast; do
  g++ -std=c++23 $f level-flag.cpp -o lv.bin
  echo "$f : $(stat -c%s lv.bin) byte"
done

Hasil Eksekusi:

text
-O0 : 22368 byte
-O1 : 16296 byte
-O2 : 16464 byte
-O3 : 16360 byte
-Os : 16464 byte
-Ofast : 17856 byte

Tiga hal patut dibaca dari angka ini. Pertama, lompatan ukuran terbesar terjadi dari -O0 ke -O1 (kode tanpa optimasi membawa banyak instruksinya sendiri). Kedua, -O2 dan -Os bisa sama besar untuk program kecil — perbedaan keduanya baru terasa pada program nyata. Ketiga, -Ofast justru paling besar karena kode tervektorisasi membawa konstanta dan instruksi tambahan. Untuk program sekecil ini perbedaan kecepatan antar level juga kecil; jangan menggeneralisasi angka ke program besar.

-Ofast: Ketika Hasil Berubah #

Sekarang bagian yang membuat -Ofast berbeda kelas. Kompilasi berkas yang sama dengan -Ofast, lalu jalankan:

bash
g++ -std=c++23 -Ofast level-flag.cpp -o lf-fast
./lf-fast

Hasil Eksekusi (platform uji):

text
jumlah 0.25f : 2500000
jumlah 0.1f  : 975599.125
build debug  : assert aktif, memeriksa stok > 0

Penjumlahan 0.1f berubah dari 1087937 menjadi 975599.125 — beda sekitar 10%. Penyebabnya -ffast-math: compiler mengatur ulang penjumlahan menjadi beberapa penjumlahan paralel (untuk vektorisasi) lalu menggabungkannya, dan penjumlahan floating-point memang tidak asosiatif — (a + b) + c tidak selalu sama dengan a + (b + c). Perhatikan baris 0.25f tidak bergerak: nilai yang terwakili persis kebal pengaturan ulang. Konsekuensinya praktis: gunakan -ffast-math hanya untuk domain yang memang toleran galat seperti grafis atau pemrosesan citra, dan hindari untuk perhitungan keuangan, ilmiah, atau akumulasi yang keakuratannya diuji.

NDEBUG dan Kebiasaan Debug vs Release #

Flag level optimasi berjalan beriringan dengan makro NDEBUG. Ketika makro ini didefinisikan, compiler menghapus seluruh assert dari kode — pemeriksaan yang bagus saat development, gratis (nol biaya runtime) saat rilis:

bash
g++ -std=c++23 -O2 -DNDEBUG level-flag.cpp -o lf-rel
./lf-rel

Hasil Eksekusi:

text
jumlah 0.25f : 2500000
jumlah 0.1f  : 1087937
build release: assert dimatikan compiler

Perhatikan hasil perhitungan tidak berubah; yang berubah hanya laporan cabang #ifdef karena assert tidak lagi dieksekusi. Dari sini timbul kebiasaan yang sehat: dua profil build untuk setiap proyek — Debug (-O0/-Og, assert aktif, sanitizer menyala) untuk pengembangan, dan Release (-O2, -DNDEBUG) untuk pengguna akhir. Uji performa selalu di build Release, karena kesimpulan dari build Debug bisa terbalik: biaya abstraksi yang terlihat di -O0 sering kali sudah dihapus optimizer di -O2.

Level optimasi mengatur seberapa cepat instruksi berjalan, tetapi tidak mengatur seberapa jauh data harus diperjalankan di dalam hierarki memori. Faktor tersembunyi itu — cache — justru sering lebih menentukan, dan itulah pokok bahasan chapter berikutnya.

Rujukan resmi: GCC Optimize Options dan assert di cppreference .