Belajar C++ - Performance & Optimization
Series/Belajar C++/Episode 15
Episode 15 of 24

Belajar C++ - Performance & Optimization

Episode ini membahas optimasi performa C++: profiling dengan perf, Valgrind, dan compiler tools, optimasi memory layout dan cache friendliness, compiler optimization flags, inline dan loop unrolling, serta tradeoff antara readability dan performance.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Pendahuluan

C++ dipilih karena performanya — tetapi performa tidak datang dengan sendirinya. Kode yang sama bisa berjalan ratusan kali lebih lambat hanya karena layout memori yang buruk atau bendera kompilasi yang salah. Kabar baiknya: C++ memberikan alat untuk mengukur dan mengoptimalkan.

Episode 15 mengajarkan cara berpikir tentang performa: ukur dulu dengan profiling sebelum mengubah apa pun, pahami bendera optimasi compiler, sadari betapa besar pengaruh cache, dan pertahankan keseimbangan antara kode yang cepat dan kode yang bisa dibaca manusia.

Ukur Dulu dengan Profiling

Profiling dengan perf

Optimasi tanpa pengukuran adalah tebakan. perf adalah profiler ringan bawaan Linux yang mengukur CPU, cache misses, dan branch mispredictions. Bangun dengan -pg atau simbol debug, lalu profil:

Profil dengan perf
g++ -g -O2 -std=c++20 app.cpp -o app
perf stat ./app
perf record -g ./app
perf report

perf stat ./app menampilkan ringkasan — berapa lama eksekusi, berapa cache misses. perf record -g ./app merekam call stack, dan perf report menunjukkan fungsi mana yang paling panas. Perhatikan kolom cache misses: angka yang tinggi menandakan masalah locality memori.

Valgrind untuk Memori dan Waktu

Valgrind memeriksa kebocoran dan kesalahan memori, dan callgrind memberikan profil yang lebih detail tanpa dukungan hardware. Jalankan:

Valgrind memeriksa memori
valgrind --leak-check=full ./app

valgrind --leak-check=full ./app melaporkan setiap blok yang bocor beserta stack trace-nya. Program berjalan jauh lebih lambat di bawah Valgrind, jadi gunakan untuk build debug. Kalian juga bisa memakai -fsanitize=address yang sudah dibahas di episode 11 sebagai alternatif yang lebih cepat.

Compiler Optimization Flags

Level Optimasi

Compiler C++ punya level optimasi yang meningkatkan kecepatan dengan mengorbankan waktu kompilasi dan ukuran biner: -O0 (tanpa optimasi, untuk debugging), -O1, -O2 (default produksi), -O3 (optimasi agresif), dan -Os (optimasi ukuran). -march=native mengaktifkan instruksi yang tersedia di CPU kalian:

Build produksi
g++ -O3 -DNDEBUG -march=native -std=c++20 app.cpp -o app

-O3 mengaktifkan optimasi agresif seperti inlining dan vectorization. -DNDEBUG menghapus semua assert — penting karena assert aktif hanya di debug. -march=native menargetkan CPU lokal, tetapi jangan dipakai untuk biner yang didistribusikan karena kode tidak akan jalan di CPU yang lebih tua.

Perhatian pada Optimasi Agresif

-O3 dan fast-math bisa mengubah perilaku program. -ffast-math mengasumsikan tidak ada NaN dan mengabaikan tanda nol, yang valid untuk simulasi numerik tetapi bisa memecah kode yang bergantung pada IEEE 754. Ukur dampaknya; jika -O2 cukup cepat, tidak perlu -O3.

Memory Layout dan Cache Friendliness

Cache Mendominasi Performa

Membaca dari RAM jauh lebih lambat daripada dari cache L1. Program yang melompat-lompat di memori menghabiskan waktu menunggu data. Kunci cache friendliness adalah locality: akses memori yang berurutan dan data yang saling dipakai diletakkan berdekatan.

Kontainer kontigu seperti std::vector menyimpan elemen bersebelahan, jadi iterasi berurutan memanfaatkan cache dengan baik. std::list yang elemennya tersebar acak justru lambat untuk iterasi meski penyisipannya O(1):

Iterasi yang ramah cache
cat > cache.cpp <<'EOF'
#include <iostream>
#include <vector>
 
int main() {
    std::vector<std::vector<int>> grid(1000, std::vector<int>(1000, 1));
    long long total = 0;
 
    for (int i = 0; i < 1000; ++i) {
        for (int j = 0; j < 1000; ++j) {
            total += grid[i][j];
        }
    }
    std::cout << total << "\n";
}
EOF
g++ -O2 -std=c++20 cache.cpp -o cache
./cache

Loop grid[i][j] dengan urutan baris-dulu membaca memori secara kontigu — ini pola row-major yang ramah cache. Membalik urutan menjadi grid[j][i] akan membuat program jauh lebih lambat karena melompat ke baris yang berjauhan.

Layout Data

Urutan field dalam struct memengaruhi ukuran dan cache. Susun field dari terbesar ke terkecil untuk mengurangi padding. Aturan ini penting untuk array struct besar yang diiterasi terus-menerus. sizeof(struct) mengungkapkan hasil nyata — optimasi kecil yang efeknya besar pada data panas.

Inline dan Loop Unrolling

Inline

Inlining menyalin tubuh fungsi ke titik pemanggilan, menghilangkan overhead panggilan. Dengan -O2 dan seterusnya, compiler memutuskan sendiri fungsi mana yang di-inline. Kata kunci inline hanyalah saran; __attribute__((always_inline)) memaksa. Aturan praktis: biarkan compiler memilih — terlalu banyak inline membengkakkan cache instruksi.

Loop Unrolling

Loop unrolling menggandakan tubuh loop sehingga setiap iterasi memproses beberapa elemen, mengurangi overhead pemeriksaan kondisi. Compiler melakukannya otomatis di -O3; manual unrolling dengan pragma tersedia jika perlu:

Pragma unrolling
#include <iostream>
#include <vector>
 
#pragma GCC unroll 4
void jumlah(const std::vector<int>& v, long long& total) {
    for (int x : v) {
        total += x;
    }
}

#pragma GCC unroll 4 meminta compiler membuka loop sebanyak 4 kali per iterasi. Sebelum memakai pragma atau mikro-optimasi lain, ukur dulu — compiler sering kali sudah menghasilkan kode yang baik tanpa campur tangan.

Tradeoff Readability vs Performance

Optimasi Terbaik adalah yang Tidak Perlu

Banyak kode "dioptimalkan" secara prematur — menulis mikro-optimasi sebelum ada bukti kebutuhan. Hasilnya kode rumit, sulit dipelihara, dan sering tidak lebih cepat karena compiler sudah menanganinya. Prinsip yang dianut industri:

  • Ukur dulu: profil sebelum mengubah apa pun.
  • Optimalkan bottleneck saja: fokus pada fungsi panas yang dominan.
  • Jaga kualitas: kode jelas lebih mudah dioptimalkan nanti.
  • Dokumentasikan alasan: komentar mengapa optimasi dilakukan.

Ada dua keputusan arsitektur yang berdampak lebih besar daripada mikro-optimasi: memilih kontainer yang tepat (episode 8) dan menghindari salinan data dengan reference serta move semantics (episode 16). Biasakan memulai dari sana.

Tip

Amdahl's law: percepatan keseluruhan dibatasi bagian yang paling lambat dan paling sering dipanggil. Profiling memberitahu kalian bagian mana yang benar-benar perlu dioptimalkan.

Penutup

Inti yang harus dibawa pulang:

  • Profil dulu dengan perf atau Valgrind sebelum mengoptimalkan apa pun.
  • -O2 adalah default produksi; -O3 dan -march=native sesuai kebutuhan.
  • Cache locality mendominasi performa; std::vector lebih ramah cache dari std::list.
  • Susun field struct dari terbesar untuk mengurangi padding.
  • Inlining dan loop unrolling sebaiknya diserahkan ke compiler.
  • Optimasi prematur adalah sumber bug; fokus pada bottleneck terukur.

Di episode 16 selanjutnya kita akan membahas modern C++ features — auto, range-based loops, dan structured bindings, move semantics dengan rvalue references, std::optional, std::variant, std::any, dan coroutines, serta modules, concepts, dan peningkatan constexpr.

Belajar C++ - Performance & Optimization | Belajar C++