Proyek C nyata biasanya membutuhkan beberapa konfigurasi build yang berbeda untuk keperluan berbeda: build Debug untuk pengembangan sehari-hari (optimasi rendah, informasi debug lengkap), build Release untuk produksi (optimasi tinggi, tanpa assertion), dan RelWithDebInfo untuk profil produksi yang tetap bisa didebug. CMake mengelola konfigurasi-konfigurasi ini melalui variabel CMAKE_BUILD_TYPE. Pada chapter ini kita akan membandingkan flag yang dihasilkan tiap konfigurasi, mengamati efek pada ukuran binary, dan melihat bagaimana macro NDEBUG mengubah perilaku runtime program.

Empat Build Type Bawaan CMake #

CMake mendefinisikan empat build type standar yang masing-masing menghasilkan set flag compiler berbeda:

Build TypeFlag CompilerTujuan
Debug-gPengembangan, optimasi rendah, debug info
Release-O3 -DNDEBUGProduksi, optimasi agresif, assertion off
RelWithDebInfo-O2 -g -DNDEBUGProduksi yang masih bisa didebug
MinSizeRel-Os -DNDEBUGProduksi, ukuran binary minimum

Variabel CMAKE_BUILD_TYPE diatur saat konfigurasi lewat -D. Setiap direktori build hanya boleh punya satu build type — bila kita ingin membangun dua konfigurasi, buat dua direktori terpisah (build_debug/ dan build_release/).

Kode Demo #

Kode berikut memeriksa apakah macro NDEBUG didefinisikan. CMake secara otomatis mendefinisikan NDEBUG untuk build type Release, RelWithDebInfo, dan MinSizeRel:

c
#include <stdio.h>

#ifdef NDEBUG
#define DEBUG_STATUS "NDEBUG defined (assertions disabled)"
#else
#define DEBUG_STATUS "NDEBUG NOT defined (assertions enabled)"
#endif

int sum_array(const int *a, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += a[i];
    }
    return total;
}

int main(void) {
    int a[1000];
    for (int i = 0; i < 1000; i++) {
        a[i] = i + 1;
    }
    int hasil = sum_array(a, 1000);
    printf("sum 1..1000 = %d\n", hasil);
    printf("build: %s\n", DEBUG_STATUS);
    return 0;
}

CMakeLists.txt minimal untuk proyek ini:

cmake
cmake_minimum_required(VERSION 3.10)
project(build-config-demo C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(app main.c)

target_compile_options(app PRIVATE -Wall -Wextra)

Build Debug #

bash
cmake -B build_debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build_debug
./build_debug/app

Output yang Dihasilkan:

text
-- Configuring done (0.1s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/test_build_config/build_debug
[ 50%] Building C object CMakeFiles/app.dir/main.c.o
[100%] Linking C executable app
[100%] Built target app
sum 1..1000 = 500500
build: NDEBUG NOT defined (assertions enabled)

Baris terakhir menunjukkan NDEBUG tidak didefinisikan — assertion (assert() dari <assert.h>) akan aktif saat runtime. Mari kita lihat command line compiler yang sebenarnya dipakai dengan memeriksa compile_commands.json:

bash
cmake -B build_debug -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cat build_debug/compile_commands.json | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['command'])"

Output yang Dihasilkan:

text
/usr/bin/cc   -g -std=gnu11 -Wall -Wextra -o CMakeFiles/app.dir/main.c.o -c /tmp/test_build_config/main.c

Flag yang aktif di build Debug:

  1. -g: informasi debug DWARF — diperlukan oleh gdb untuk menampilkan nama variabel dan nomor baris.
  2. -std=gnu11: CMake menambahkan flag standar C (dialek gnu11 karena CMAKE_C_EXTENSIONS default ON).
  3. -Wall -Wextra: flag dari target_compile_options di CMakeLists.txt.
  4. Tidak ada -O: tidak ada optimasi. Variabel lokal disimpan di stack persis seperti tertulis di kode — mudah diperiksa di debugger.

Build Release #

bash
cmake -B build_release -DCMAKE_BUILD_TYPE=Release
cmake --build build_release
./build_release/app

Output yang Dihasilkan:

text
sum 1..1000 = 500500
build: NDEBUG defined (assertions disabled)

Periksa command line compiler:

bash
cat build_release/compile_commands.json | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['command'])"

Output yang Dihasilkan:

text
/usr/bin/cc   -O3 -DNDEBUG -std=gnu11 -Wall -Wextra -o CMakeFiles/app.dir/main.c.o -c /tmp/test_build_config/main.c

Perbedaan utama dengan Debug:

  1. -O3: optimasi agresif — loop unrolling, function inlining, vectorization. Kode berjalan lebih cepat tetapi susah didebug karena variabel dioptimasi keluar dari stack.
  2. -DNDEBUG: mendefinisikan macro NDEBUG. Header <assert.h> memeriksa macro ini — bila NDEBUG terdefinisi, assert(expr) dievaluasi menjadi kosong (tidak ada kode runtime). Ini menghilangkan overhead assertion di produksi.
  3. Tidak ada -g: tidak ada informasi debug. Ukuran binary lebih kecil.

Build RelWithDebInfo #

bash
cmake -B build_relwithdebinfo -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build_relwithdebinfo
./build_relwithdebinfo/app

Output yang Dihasilkan:

text
sum 1..1000 = 500500
build: NDEBUG defined (assertions disabled)

Periksa command line:

bash
cat build_relwithdebinfo/compile_commands.json | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['command'])"

Output yang Dihasilkan:

text
/usr/bin/cc   -O2 -g -DNDEBUG -std=gnu11 -Wall -Wextra -o CMakeFiles/app.dir/main.c.o -c /tmp/test_build_config/main.c

RelWithDebInfo adalah kompromi antara Debug dan Release:

  1. -O2: optimasi standar rilis — cepat tetapi tidak se-agresif -O3.
  2. -g: informasi debug disertakan — kita bisa memakai gdb untuk memeriksa crash di produksi (meskipun beberapa variabel mungkin sudah dioptimasi keluar).
  3. -DNDEBUG: assertion dimatikan seperti Release.

Build type ini cocok untuk profil production crash investigation — kita mengirim binary dengan info debug ke lapangan, lalu saat crash terjadi, gdb bisa menampilkan stack trace dengan nomor baris.

Build MinSizeRel #

bash
cmake -B build_minsizerel -DCMAKE_BUILD_TYPE=MinSizeRel
cmake --build build_minsizerel
./build_minsizerel/app

Output yang Dihasilkan:

text
sum 1..1000 = 500500
build: NDEBUG defined (assertions disabled)

Periksa command line:

bash
cat build_minsizerel/compile_commands.json | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['command'])"

Output yang Dihasilkan:

text
/usr/bin/cc   -Os -DNDEBUG -std=gnu11 -Wall -Wextra -o CMakeFiles/app.dir/main.c.o -c /tmp/test_build_config/main.c

MinSizeRel memakai -Os — optimasi untuk ukuran kode terkecil. Compiler menghindari optimasi yang memperbesar binary (loop unrolling, function inlining agresif). Cocok untuk embedded device dengan flash storage terbatas atau distribusi binary ke jaringan lambat.

Perbandingan Ukuran Binary #

Bandingkan ukuran executable yang dihasilkan tiap build type:

bash
ls -l build_debug/app build_release/app build_relwithdebinfo/app build_minsizerel/app | awk '{print $5, $9}'

Output yang Dihasilkan:

text
17560 build_debug/app
18552 build_relwithdebinfo/app
15984 build_minsizerel/app
15984 build_release/app

Mari kita analisis angka-angka tersebut:

  1. Debug (17560 byte): paling besar di antara yang tanpa optimasi. Incl. info debug DWARF yang menyimpan mapping baris, tabel simbol, dan tipe data. Instruksi mesin tidak dioptimasi sehingga sedikit lebih panjang dari -O2.
  2. Release (15984 byte): ukuran terkecil karena -O3 mengoptimalkan kode. RelWithDebInfo 2,5 KB lebih besar dari Release murni karena info debug.
  3. MinSizeRel (15984 byte): ukuran sama dengan Release di proyek kecil ini. Pada proyek besar, MinSizeRel biasanya menghasilkan binary 5-15% lebih kecil dari Release karena tidak melakukan loop unrolling.
  4. RelWithDebInfo (18552 byte): paling besar karena gabungan optimasi dan info debug. Tambahan 2,5 KB dibanding Release adalah tabel DWARF untuk memetakan instruksi ke baris kode sumber.