Program yang sama, dikompilasi dengan flag berbeda, bisa menjadi dua biner yang bertingkah laku berbeda — dan inilah salah satu sumber bug paling licik di C++. Saat mengembangkan kita menginginkan build yang cepat dikompilasi dan ramah debugger; saat mengirim ke pengguna kita menginginkan build yang cepat berjalan dan kecil. Dunia build system menyebut keduanya konfigurasi Debug dan Release, dan keduanya dibedakan oleh sekumpulan flag compiler yang setiap anggotanya layak Anda kenali satu per satu: -O untuk tingkat optimasi, -g untuk informasi debug, dan -DNDEBUG yang diam-diam menghapus pemeriksaan assert. Semua flag di chapter ini diuji langsung dengan g++ di lingkungan praktikum.

Debug dan Release di CMake #

CMake membungkus konsep ini dalam variabel CMAKE_BUILD_TYPE yang dipilih saat configure:

bash
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake -B build -DCMAKE_BUILD_TYPE=Release

Tiap nama memetakan ke set flag compiler bawaan — untuk GCC di sistem mirip Unix:

CMAKE_BUILD_TYPEFlag GCCTujuan
Debug-gPengembangan: berisi informasi debug, tanpa optimasi
Release-O3 -DNDEBUGDistribusi: optimasi penuh, pemeriksaan dihapus
RelWithDebInfo-O2 -g -DNDEBUGRelease yang masih bisa di-debug
MinSizeRel-Os -DNDEBUGUkuran file terkecil

Perhatikan bahwa -DNDEBUG hadir di ketiga konfigurasi non-Debug — bukan kebetulan, dan nanti kita buktikan akibatnya. Tanpa pemilihan eksplisit, CMake tidak menetapkan build type apa pun (flag kosong), kebiasaan yang mengejutkan banyak pemula: proyek yang “berjalan lambat sekali” sering kali hanya belum diberi CMAKE_BUILD_TYPE=Release.

Tingkat Optimasi: -O0 sampai -Os #

Flag -O mengatur seberapa jauh compiler boleh menyusun ulang kode kita demi kecepatan atau ukuran:

FlagArtiKhas dipakai untuk
-O0Tanpa optimasi — kode dieksekusi apa adanyaDebug; kompilasi tercepat
-O1Optimasi ringanKompromi kecepatan kompilasi/hasil
-O2Optimasi standar — inlining, unrolling, vektorisasi dasarKebanyakan software rilis
-O3Optimasi agresif — vektorisasi lebih jauhProgram yang dibatasi kecepatan hitung
-OsOptimasi untuk ukuran, bukan kecepatanFirmware, alat dengan disk/memori kecil

Untuk melihat efeknya, kita pakai program kecil yang menghitung jumlah bilangan prima di bawah 50.000 — murni aritmetika integer, sehingga hasilnya wajib identik di semua tingkat optimasi:

cpp
#include <iostream>

// Menghitung berapa banyak bilangan prima di bawah batas.
// Murni aritmetika integer: hasilnya identik pada semua tingkat -O.
bool apakahPrima(int n) {
    if (n < 2) return false;
    for (int p = 2; p * p <= n; ++p) {
        if (n % p == 0) return false;
    }
    return true;
}

int main() {
    const int batas = 50000;
    int jumlah = 0;
    for (int n = 2; n < batas; ++n) {
        if (apakahPrima(n)) ++jumlah;
    }
    std::cout << "Bilangan prima di bawah " << batas << ": " << jumlah << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Bilangan prima di bawah 50000: 5133

Kini kompilasi empat varian dan jalankan semuanya — log nyata dari lingkungan praktikum:

bash
g++ -std=c++23 -Wall -Wextra -O0 -g demo-optimasi.cpp -o demo-debug
g++ -std=c++23 -Wall -Wextra -O2 demo-optimasi.cpp -o demo-O2
g++ -std=c++23 -Wall -Wextra -O3 demo-optimasi.cpp -o demo-O3
g++ -std=c++23 -Wall -Wextra -Os demo-optimasi.cpp -o demo-Os
./demo-debug
./demo-O2
./demo-O3
./demo-Os

Output yang Dihasilkan:

text
Bilangan prima di bawah 50000: 5133
Bilangan prima di bawah 50000: 5133
Bilangan prima di bawah 50000: 5133
Bilangan prima di bawah 50000: 5133

Keempat biner menghitung sama persis — optimasi tidak boleh mengubah hasil. Lalu bagaimana wujud fisiknya? Kolom ukuran dari ls -l (beserta namanya, dipilih lewat awk) pada build yang sama:

bash
ls -l demo-debug demo-O2 demo-O3 demo-Os | awk '{print $5, $9}'

Output yang Dihasilkan:

text
16224 demo-O2
16248 demo-O3
16224 demo-Os
104896 demo-debug

Biner debug membengkak hingga enam kali lipat dibanding varian optimasi — sebagian besar karena -g. Untuk program sekecil ini selisih -O2, -O3, dan -Os tipis; pola perbedaannya yang penting diingat, bukan angka byte-nya (yang di mesin Anda bisa sedikit berbeda karena versi compiler). Pengukuran kecepatan eksekusi antar varian kita serahkan ke Section Performa dan Optimasi yang punya alat ukurnya sendiri.

Informasi Debug dengan -g #

Flag -g tidak mengubah cara program berjalan sama sekali — ia menambah informasi debug: tabel yang memetakan kode mesin kembali ke baris source, nama variabel, dan tipe. Database inilah yang dibaca debugger untuk menampilkan “Anda sedang di baris 17, variabel jumlah bernilai 5133”. Bayarnya ukuran file:

bash
g++ -std=c++23 -Wall -Wextra -O0 demo-optimasi.cpp -o demo-O0
ls -l demo-O0 demo-debug | awk '{print $5, $9}'

Output yang Dihasilkan:

text
22096 demo-O0
104896 demo-debug

Tanpa -g, biner -O0 hanya 22.096 byte; dengan -g, 104.896 byte. Karena informasi debug tidak berpengaruh ke perilaku, kebiasaan yang sehat adalah: -g selalu menyertai build pengembangan, dan build distribusi yang butuh penelusuran masalah di lapangan memilih RelWithDebInfo — optimasi penuh dengan info debug tetap dibawa.

NDEBUG: Ketika assert Ikut Hilang #

Makro assert — pemeriksaan kondisi yang menghentikan program bila kondisinya salah — hanya aktif ketika makro NDEBUG tidak didefinisikan. Kompilasi Release mendefinisikannya, sehingga seluruh ekspresi assert(...) diganti baris kosong oleh preprocessor. Program berikut memakai assert untuk menagih validitas input:

cpp
#include <cassert>
#include <iostream>
#include <string>

int main(int argc, char* argv[]) {
    int skor = (argc > 1) ? std::stoi(argv[1]) : 85;

    // Pemeriksaan input yang wajib benar: skor berada pada rentang 0..100.
    assert(skor >= 0 && skor <= 100);

    std::cout << "Skor tercatat: " << skor << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Skor tercatat: 85

Kini kompilasi dua varian dan uji keduanya dengan input yang melanggar kontrak — skor 150:

bash
g++ -std=c++23 -Wall -Wextra -O0 -g cek-batas.cpp -o cek-debug
g++ -std=c++23 -Wall -Wextra -O2 -DNDEBUG cek-batas.cpp -o cek-release
./cek-debug 150
echo $?

Output yang Dihasilkan:

text
cek-debug: cek-batas.cpp:9: int main(int, char**): Assertion `skor >= 0 && skor <= 100' failed.
134

Versi debug berhenti paksa di baris 9 — nama file dan nomor barisnya menunjuk sumber pada saat kompilasi — dengan exit code 134 (128 ditambah kode sinyal SIGABRT; pesan Aborted yang menyertainya tampil beda antar shell). Sekarang biner release dengan input yang sama:

bash
./cek-release 150
echo $?

Output yang Dihasilkan:

text
Skor tercatat: 150
0

Pemeriksaannya hilang begitu saja: skor 150 lolos dan program kembali dengan sukses. Inilah wajah dua sisi NDEBUG — build debug berperan sebagai jaring pengaman di tangan developer, build release tidak lagi membayar biaya pemeriksanya di produksi.

Memilih Konfigurasi #

  • Sehari-hari menulis kode: Debug — jaring pengaman assert penuh, debugger siap pakai, kompilasi cepat.
  • Continuous integration: build Debug untuk menjalankan tes, plus Release untuk memastikan versi optimasi juga sehat.
  • Mengirim ke pengguna: Release atau MinSizeRel; RelWithDebInfo bila laporan dari lapangan perlu ditelusuri.

Konfigurasi, dependensi, dan tes kini sudah lengkap sebagai bagian-bagian terpisah. Yang belum tergambar adalah wujud keseluruhan proyek: file mana taruhnya di folder mana, supaya semua bagian itu menemukan satu sama lain tanpa kekacauan. Chapter berikutnya, yang sekaligus menutup section ini, menyusun layout-nya.

Referensi: Optimize Options , Debugging Options , assert — cppreference .