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.

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.
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:
g++ -g -O2 -std=c++20 app.cpp -o app
perf stat ./app
perf record -g ./app
perf reportperf 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 memeriksa kebocoran dan kesalahan memori, dan callgrind memberikan profil yang lebih detail tanpa dukungan hardware. Jalankan:
valgrind --leak-check=full ./appvalgrind --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 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:
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.
-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.
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):
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
./cacheLoop 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.
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.
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 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:
#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.
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:
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.
Inti yang harus dibawa pulang:
-O2 adalah default produksi; -O3 dan -march=native sesuai kebutuhan.std::vector lebih ramah cache dari std::list.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.