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 #
| Flag | Ringkasan | Kapan dipakai |
|---|---|---|
-O0 | tanpa optimasi, setia pada baris sumber | debugging harian |
-Og | optimasi ringan yang tidak mengganggu debugging | siklus edit–compile–debug |
-O1 | optimasi dasar tanpa trade-off ruang-waktu | build cepat untuk pengujian |
-O2 | optimasi standar: inlining, vectorization hemat, dsb. | default build rilis |
-O3 | lebih agresif (unrolling lebih lebar, loop lebih dalam) | hot path terukur, ukuran lebih besar |
-Os | optimasi dengan fokus ukuran binary | perangkat kecil, cache instruksi ketat |
-Ofast | -O3 + -ffast-math + pelonggaran standar | hanya 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:
// 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):
jumlah 0.25f : 2500000
jumlah 0.1f : 1087937
build debug : assert aktif, memeriksa stok > 0Bedah baris penting:
eksakdengan0.25f: 0.25 dapat diwakili persis dalam binary float, sehingga hasilnya2500000bulat sempurna di semua level optimasi — operasi urutan mana pun memberi hasil sama.kiradengan0.1f: 0.1 tidak terwakili persis, jadi tiap penambahan membawa galat pembulatan; hasil1087937adalah akumulasi galat urutan penjumlahan berurutan.assert(stok > 0): pada build tanpaNDEBUG, pemeriksaan benar-benar dieksekusi, dan cabang#ifdefmelaporkan build debug.
Ukurannya? Kompilasi berkas yang sama di enam level menghasilkan binary berbeda (platform uji; angka byte milik program ini saja):
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"
doneHasil Eksekusi:
-O0 : 22368 byte
-O1 : 16296 byte
-O2 : 16464 byte
-O3 : 16360 byte
-Os : 16464 byte
-Ofast : 17856 byteTiga 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:
g++ -std=c++23 -Ofast level-flag.cpp -o lf-fast
./lf-fastHasil Eksekusi (platform uji):
jumlah 0.25f : 2500000
jumlah 0.1f : 975599.125
build debug : assert aktif, memeriksa stok > 0Penjumlahan 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:
g++ -std=c++23 -O2 -DNDEBUG level-flag.cpp -o lf-rel
./lf-relHasil Eksekusi:
jumlah 0.25f : 2500000
jumlah 0.1f : 1087937
build release: assert dimatikan compilerPerhatikan 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 .