Setelah Chapter sebelumnya meyakinkan kita bahwa biaya harus dihitung, pertanyaannya bergeser: bagian mana dari program nyata yang paling mahal? Tebakan hampir selalu keliru — programmer cenderung mengoptimasi bagian yang terasa berat, padahal kemacetan sering bersembunyi di loop yang tampak sepele. Profiler adalah alat yang menjawab pertanyaan ini secara empiris: program dijalankan seperti biasa, sementara alat mencatat di fungsi mana waktu prosesor benar-benar habis. Chapter ini melatih tiga cara profiling, dari yang paling sederhana hingga memakai tool bawaan sistem.

Dua Keluarga Profiler: Sampling dan Instrumentation #

Semua profiler bekerja dengan salah satu dari dua prinsip. Profiler sampling menghentikan program berkali-kali secara acak (misalnya seratus kali per detik) lalu mencatat posisi program saat itu; semakin sering sebuah fungsi muncul dalam sampel, semakin besar porsinya. Kelebihannya, program berjalan dengan kecepatan hampir normal, tetapi hasilnya berupa statistik yang bisa bergetar antar eksekusi. Profiler instrumentation bekerja sebaliknya: kode pengukur disisipkan ke setiap fungsi (atau kita tulis sendiri secara manual), sehingga setiap pemanggilan tercatat pasti — jumlah pemanggilan selalu eksak, tetapi program melambat karena membawa beban pencatatannya sendiri.

Profiler Manual: Sisipkan Pencatat Sendiri #

Cara termurah untuk profiling adalah menyisipkan variabel penghitung pada fungsi yang dicurigai. Pendekatan ini instrumentation murni, dan angkanya deterministik sehingga layak diverifikasi:

cpp
// Instrumentasi manual: kita sisipkan pencatat (counter) pada titik
// yang dicurigai mahal, lalu jalankan program dan baca angkanya.
// Angka pemanggilan dan perbandingan deterministik, tidak tergantung mesin.
#include <iostream>

long long jumlahPanggilan = 0;

long fib(int n) {
    ++jumlahPanggilan;                 // pencatat: setiap masuk fungsi
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// Pencarian pada satu juta elemen; target diletakkan paling belakang
// agar biaya terburuknya terlihat.
long long cariDenganPencatat(const int* data, int n, int target,
                             long long& perbandingan) {
    for (int i = 0; i < n; ++i) {
        ++perbandingan;
        if (data[i] == target) return i;
    }
    return -1;
}

int main() {
    long hasil = fib(25);
    std::cout << "fib(25) = " << hasil << '\n';
    std::cout << "pemanggilan fib  : " << jumlahPanggilan << '\n';

    static int data[1000000];
    for (int i = 0; i < 1000000; ++i) data[i] = i * 2;
    long long perbandingan = 0;
    long long posisi = cariDenganPencatat(data, 1000000, 1999998, perbandingan);
    std::cout << "posisi target    : " << posisi << '\n';
    std::cout << "perbandingan     : " << perbandingan << '\n';
    return 0;
}

Output yang Dihasilkan:

text
fib(25) = 75025
pemanggilan fib  : 242785
posisi target    : 999999
perbandingan     : 1000000

Bedah baris penting:

  1. jumlahPanggilan global: setiap masuk ke fib, pencatat bertambah satu. Untuk fib(25) yang naive, 242.785 pemanggilan untuk satu angka — inilah bukti profil: fungsi rekursif ini dipanggil jauh lebih sering daripada yang diperkirakan.
  2. perbandingan sebagai parameter long long&: pencarian pada satu juta elemen dengan target paling belakang mencatat 1.000.000 perbandingan, sesuai prediksi biaya terburuk O(n).
  3. Kedua angka berdiri sendiri tanpa stopwatch: kita mengetahui berapa banyak pekerjaan terjadi; lama waktu bisa kita taksir dari jumlah operasi kali biaya per operasi.

Kekurangan profiler manual jelas: kita harus menebak dulu di mana perlu memasang pencatat, dan kode produksi jadi kotor bila pencatatannya lupa dibersihkan. Untuk gambaran menyeluruh, kita perlu alat yang mengukur seluruh program sekaligus.

gprof: Profiler Sampling dari GNU Binutils #

gprof ikut terpasang bersama binutils GCC, jadi biasanya sudah ada di Linux. Caranya dua langkah: kompilasi dengan flag -pg, jalankan program seperti biasa (menghasilkan berkas gmon.out), lalu minta gprof membacanya:

bash
g++ -std=c++23 -Wall -Wextra -O0 -pg profil-gprof.cpp -o profil-gprof
./profil-gprof
gprof ./profil-gprof gmon.out | head -8

Hasil Eksekusi (satu contoh nyata di platform uji; angka persentase bergetar antar eksekusi):

text
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 20.47      0.09     0.09        3    30.03    45.04  hitungModTiga(std::vector<int, std::allocator<int> > const&)
 18.20      0.17     0.08        3    26.69    41.71  hitungAmbang(std::vector<int, std::allocator<int> > const&)
 13.65      0.23     0.06                             _init

Kolom yang penting dibaca: % time (porsi waktu di fungsi itu), self seconds (waktu di badan fungsi sendiri), calls (jumlah pemanggilan, eksak karena berbasis instrumentation panggilan), dan name (nama fungsi beserta tanda tangannya). Pada contoh ini hitungModTiga menempati porsi terbesar — operasi % jauh lebih mahal daripada perbandingan < yang dikerjakan hitungAmbang pada data yang sama. Sampel ini diambil dengan -O0 secara sengaja: pada -O2, compiler menempelkan banyak fungsi ke pemanggilnya (inlining), sehingga gprof kehilangan batas antar fungsi dan laporannya menyatu ke main. Bila Anda terpaksa memprofil build teroptimasi, matikan inlining dengan -fno-inline-functions dan tafsirkan hasilnya hati-hati.

Peta Tools Lanjutan dan Pola chrono #

Di luar gprof, ekosistem Linux menyediakan perf (profiler sampling berbasis counter hardware, dengan flame graph), serta valgrind --tool=callgrind (instrumentation penuh yang menghitung instruksi satu per satu — sangat teliti tetapi program melambat belasan kali). Keduanya tidak terpasang di sandbox tutorial ini, sehingga tidak bisa kita jalankan di sini; cara pakainya nanti Anda coba langsung di mesin sendiri. Sementara itu, untuk pertanyaan sempit “berapa lama bagian ini berjalan”, pola std::chrono tiga langkah sudah cukup:

cpp Pola pengukuran durasi dengan chrono — angka keluaran bervariasi antar mesin dan antar eksekusi
#include <chrono>
#include <iostream>

int main() {
    auto mulai = std::chrono::steady_clock::now();
    long long total = 0;
    for (int i = 0; i < 10000000; ++i) total += i % 7;
    auto selesai = std::chrono::steady_clock::now();
    auto durasi = std::chrono::duration_cast<std::chrono::milliseconds>(
        selesai - mulai);
    std::cout << "selesai dalam " << durasi.count() << " ms\n";
    return 0;
}

Ingat keterbatasannya: steady_clock mengukur waktu nyata (wall-clock), jadi hasilnya dipengaruhi beban sistem dan tidak boleh dijadikan angka yang dikutip sebagai fakta — gunakan untuk perbandingan kasar A versus B di mesin yang sama, seperti dijelaskan pada chapter Benchmarking. Dengan profiler dan pencatat manual, Anda kini bisa menyebut nama kemacetan dengan bukti. Tapi begitu kemacetan ditemukan di fungsi yang seharusnya cepat, tersangka berikutnya adalah compiler: mungkinkah kode kita belum memberi kesempatan penuh untuk dioptimasi? Itulah topik chapter berikutnya.

Rujukan resmi: std::chrono dan GCC Instrumentation Options .