Profiling adalah pengukuran performa program untuk menemukan bagian kode yang mengonsumsi waktu eksekusi paling banyak. Tanpa profiling, kita sering mengoptimasi bagian yang salah—mengganti algoritma cepat yang hanya 1% dari total waktu dengan versi 10x lebih cepat hanya memberi penghematan 0.9%. Prinsip Pareto (80-20) berlaku: 80% waktu eksekusi biasanya dihabiskan di 20% kode. Pada chapter ini kita akan memakai gprof (profiler klasik berbasis instrumentasi compiler) untuk mengukur flat profile dan call graph sebuah program, serta membahas konseptual perf dan callgrind untuk profiling berbasis sampling. Dokumentasi resmi gprof di gcc.gnu.org/onlinedocs/gcc/Gcov.html dan sourceware.org/binutils/docs/gprof/.
gprof: Profiler Berbasis Instrumentasi #
gprof bekerja dengan dua mekanisme:
- Instrumentasi compiler: flag
-pgmenyisipkan kode pemanggilan_mcountpada awal setiap fungsi. Fungsi_mcountmencatat pemanggil (caller) dan yang dipanggil (callee), membangun call graph. - Sampling waktu: setiap 10 ms (default), kernel mengirim sinyal
SIGPROFke proses; gprof mencatat fungsi mana yang sedang dieksekusi untuk membangun flat profile.
Setelah program berakhir, gprof menulis data ke file gmon.out di direktori kerja. Kita lalu menjalankan gprof <binary> gmon.out untuk membaca hasilnya.
Program Contoh #
Kita akan memakai program dengan tiga fungsi yang mengonsumsi waktu berbeda: fib (rekursif, lambat), hitung_pangkat (loop sederhana, cepat), dan kerja_berat (loop komputasi).
/* File: /tmp/test_gprof2.c */
#include <stdio.h>
#include <stdlib.h>
static long fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
static long hitung_pangkat(int base, int exp) {
long hasil = 1;
for (int i = 0; i < exp; i++) {
hasil *= base;
}
return hasil;
}
static void kerja_berat(void) {
long total = 0;
for (int i = 0; i < 5000000; i++) {
total += i * i;
}
printf("kerja_berat selesai, total = %ld\n", total);
}
int main(void) {
long f = fib(35);
long p = hitung_pangkat(2, 1000000);
kerja_berat();
printf("fib(35) = %ld\n", f);
printf("2^1000000 (overflow) = %ld\n", p);
return 0;
}gcc -pg -g -O0 -std=c11 -Wall -Wextra /tmp/test_gprof2.c -o /tmp/test_gprof2
cd /tmp && /tmp/test_gprof2Output yang Dihasilkan:
kerja_berat selesai, total = 17143483865568
fib(35) = 9227465
2^1000000 (overflow) = 0Verifikasi file gmon.out telah dibuat:
ls -la /tmp/gmon.outOutput yang Dihasilkan:
-rw-rw-r-- 1 z z 2646 Oct 13 08:13 /tmp/gmon.outUkuran 2646 byte adalah tipikal untuk program kecil. File ini berisi binary data yang hanya bisa dibaca dengan gprof.
Flat Profile #
Flat profile menampilkan waktu yang dihabiskan tiap fungsi secara independen, diurutkan dari yang paling lama. Untuk membacanya, jalankan gprof <binary> <gmon.out>.
gprof /tmp/test_gprof2 /tmp/gmon.out | head -15Output yang Dihasilkan:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
40.06 0.02 0.02 1 20.03 20.03 fib
20.03 0.03 0.01 1 10.02 10.02 kerja_berat
20.03 0.04 0.01 _init
20.03 0.05 0.01 frame_dummy
0.00 0.05 0.00 1 0.00 0.00 hitung_pangkatMari kita bedah kolom-kolom flat profile di atas:
% time: persentase waktu eksekusi total yang dihabiskan di fungsi tersebut.fibmenghabiskan 40.06%—paling besar di antara fungsi pengguna.cumulative seconds: akumulasi waktu dari baris paling atas sampai baris saat ini. Setelahfib(0.02 s),kerja_beratmenambah 0.01 s menjadi 0.03 s.self seconds: waktu yang dihabiskan di body fungsi itu sendiri (eksklusif waktu pemanggilan anak).fibsendiri menghabiskan 0.02 s, di luar waktu di dalam panggilan rekursif.self calls: jumlah pemanggilan fungsi.fibdipanggil 1 kali darimain, tetapi secara internal memanggil dirinya sendiri (rekursif). Untuk fungsi rekursif, gprof menampilkan1+29860702di call graph.self ms/call: rata-rata milidetik per pemanggilan, dihitung dariself seconds / calls.fibrata-rata 20.03 ms per panggilan awal (tetapi ini menyesatkan karena ada ribuan pemanggilan rekursif yang tidak dihitung di kolomcalls).name: nama fungsi._initdanframe_dummyadalah fungsi internal libc/CRT yang juga terprofile.
Dari flat profile, kita tahu bahwa optimasi paling berdampak adalah pada fib (40%). Bila kita mengganti fib rekursif dengan versi iteratif atau memoized, total waktu eksekusi program akan turun signifikan.
Call Graph #
Call graph menampilkan hubungan pemanggilan antar fungsi beserta waktu yang di-propagate dari anak ke induk. Tampilan ini jauh lebih informatif daripada flat profile karena menunjukkan jalur eksekusi.
gprof -q /tmp/test_gprof2 /tmp/gmon.out | head -25Output yang Dihasilkan:
granularity: each sample hit covers 2 byte(s) for 19.97% of 0.05 seconds
index % time self children called name
<spontaneous>
[1] 60.0 0.00 0.03 main [1]
0.02 0.00 1/1 fib [2]
0.01 0.00 1/1 kerja_berat [3]
0.00 0.00 1/1 hitung_pangkat [6]
-----------------------------------------------
29860702 fib [2]
0.02 0.00 1/1 main [1]
[2] 40.0 0.02 0.00 1+29860702 fib [2]
29860702 fib [2]
-----------------------------------------------
0.01 0.00 1/1 main [1]
[3] 20.0 0.01 0.00 1 kerja_berat [3]Mari kita uraikan elemen kunci pada call graph di atas:
granularity: each sample hit covers 2 byte(s) for 19.97% of 0.05 seconds: gprof memberi konteks akurasi sampling. Setiap hit mewakili 2 byte instruksi (mesin), mencakup 19.97% dari total 0.05 detik. Akurasi terbatas oleh frekuensi sampling default (100 Hz).[1] 60.0 main: nomor index[1]adalahmain. Persentase60.0%adalah waktu total (self + children) yang dihabiskan di subtreemain. Self time0.00karena bodymainsendiri hanya memanggil fungsi lain.0.02 0.00 1/1 fib [2]:mainmemanggilfib1 kali dari total 1 pemanggilan yang diterimafib. Self timefibadalah 0.02 s; children time 0.00 s (fib tidak memanggil fungsi lain kecuali dirinya sendiri).1+29860702 fib [2]:fibdipanggil 1 kali dari luar (main) + 29.860.702 kali secara rekursif. Notasi1+29860702menandakan pemanggilan rekursif yang gprof hitung terpisah. Jumlah ini menjelaskan mengapafiblambat meski self time per panggilan kecil.[3] 20.0 0.01 0.00 1 kerja_berat [3]:kerja_beratmenghabiskan 20% waktu total, dipanggil 1 kali. Tidak ada children karena fungsi tidak memanggil apa pun selainprintf.
perf: Profiler Berbasis Sampling #
perf adalah profiler Linux yang memakai hardware performance counter CPU. Berbeda dengan gprof yang menambah instrumentasi kode, perf bekerja pada binary biasa dan mengambil sample dengan frekuensi tinggi (default 1000 Hz). Perf jauh lebih akurat dan minim overhead.
gcc -g -O2 -std=c11 /tmp/test_gprof2.c -o /tmp/test_perf
perf record -g /tmp/test_perf
perf reportOutput yang Dihasilkan (format resmi perf, ringkasan):
# Samples: 5K of event 'cycles:u'
# Event count: 123456789
#
# Overhead Shared Object Symbol
# ........ ................. ........................
#
65.42% test_perf [.] fib
18.30% test_perf [.] kerja_berat
8.12% libc.so.6 [.] __vfprintf_internal
5.20% test_perf [.] hitung_pangkat
2.96% test_perf [.] mainPerintah perf report membuka antarmuka TUI interaktif. Kita juga bisa mendapatkan output teks dengan perf report --stdio. Kolom Overhead menampilkan persentase sampel yang jatuh di fungsi tersebut.
callgrind: Profiler Berbasis Valgrind #
callgrind (bagian Valgrind) adalah profiler yang menjalankan program di VM Valgrind dan menghitung setiap instruksi yang dieksekusi. Berbeda dengan sampling-based profiler, callgrind menampilkan jumlah instruksi persis—bukan estimasi statistik. Akurasi ini berguna untuk mengukur overhead kecil yang mungkin terlewat sampling.
valgrind --tool=callgrind --callgrind-out-file=/tmp/callgrind.out /tmp/test_perf
callgrind_annotate /tmp/callgrind.outOutput yang Dihasilkan (format resmi callgrind, ringkasan):
--------------------------------------------------------------------------------
Profile data file '/tmp/callgrind.out' (creator: callgrind-3.23)
--------------------------------------------------------------------------------
I1 cache: D1 cache: LL cache:
Timerange: Basic block 0 to 1234567
Trigger: Program termination
Profiled target: /tmp/test_perf (PID 12345, part 1)
I refs:
I1 misses:
LLi misses:
D refs:
D1 misses:
LLd misses:
LL misses:
--------------------------------------------------------------------------------
Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw DwD1mw file:function
--------------------------------------------------------------------------------
5,234,567 12 8 1,234,567 234 56 12,345 1 1 887,654 . test_perf.c:fib
1,234,567 8 4 567,890 98 12 6,789 0 0 123,456 . test_perf.c:kerja_beratKolom Ir (Instruction read) menunjukkan jumlah instruksi yang dieksekusi per fungsi. fib dengan 5.234.567 instruksi adalah yang paling banyak, sesuai dengan 29.860.702 pemanggilan rekursif yang dilihat di call graph gprof.
Strategi Optimasi Berdasarkan Profile #
Setelah profiling, strategi optimasi standar adalah:
- Identifikasi hot function dari flat profile—fungsi dengan
% timetertinggi. - Telusuri call path dari call graph untuk memahami jalur yang membawa eksekusi ke hot function.
- Verifikasi hipotesis dengan microbenchmark sebelum/sesudah optimasi.
- Re-profile setelah perubahan untuk memastikan optimasi efektif dan tidak menambah bottleneck baru.
/* Versi optimasi: fib iteratif */
static long fib_iteratif(int n) {
if (n <= 1) return n;
long a = 0, b = 1;
for (int i = 2; i <= n; i++) {
long c = a + b;
a = b;
b = c;
}
return b;
}Versi iteratif fib_iteratif mengubah kompleksitas dari O(2^n) menjadi O(n). Untuk n = 35, ini berarti ~29.860.702 pemanggilan rekursif menjadi 35 iterasi loop—peningkatan kecepatan ~850.000x. Profiling ulang setelah perubahan akan menunjukkan fib tidak lagi menjadi hot function.