Tidak semua loop harus lenyap — loop yang tepat tetap sah setelah kita vectorize semua yang bisa. Yang per kita sapu adalah loop yang membocorkan performa lewat kebiasaan kecil: memanggil fungsi skalar berjuta kali, menghitung konstanta yang tak pernah berubah, atau menelusuri rantai field struct pada tiap iterasi. Chapter ini membedah anti-pola paling umum beserta perbaikannya dan pengukurannya. Cakupan materinya:
- Operasi skalar dalam loop → panggilan fungsi vector
- Konstanta dan lookup mahal yang dihitung ulang tiap iterasi
- Indexing berantai
a.b.c(i)→ cache handle ifyang tak pernah berubah → hoist keluar loop- Update plot dan pemanggilan fungsi tiny dalam loop
Peta Anti-Pola #
| Anti-pola | Dampak | Solusi |
|---|---|---|
Array tumbuh dalam loop (a = [a, x]) | penyalinan O(n²) | preallocation (chapter sebelumnya) |
Operasi skalar dalam loop (y(k) = sin(x(k))) | panggilan fungsi n kali | satu panggilan vector sin(x) |
| Konstanta dihitung dalam loop | ekspresi tetap dievaluasi n kali | hitung sekali sebelum loop |
Indexing berantai data.konfig.bobot(k) | resolusi field berulang | cache handle sebelum loop |
if dengan kondisi tetap dalam loop | cabang dicek n kali | putuskan sekali di luar, hoist |
Update plot/figure tiap iterasi | rendering jauh lebih mahal dari komputasi | batch / batasi drawnow |
| Fungsi tiny dipanggil jutaan kali | overhead panggilan menumpuk | inlining / vectorize |
Operasi Skalar dalam Loop #
Aturannya: pemanggilan fungsi elemen-per-elemen (sin, exp, abs, sqrt) di dalam loop diganti satu panggilan pada seluruh array. Mekanismenya: setiap pemanggilan fungsi membayar biaya dispatch — pencarian fungsi, penyiapan panggilan, validasi argumen; dikalikan 200000 iterasi, biaya tetap itu mengalahkan pekerjaan matematikanya sendiri. Versi vector membayar biaya dispatch sekali:
n = 200000;
x = linspace(0, pi, n);
tic;
y1 = zeros(1, n);
for k = 1:n
y1(k) = sin(x(k)); % anti-pola: sin dipanggil n kali per skalar
end
tBuruk = toc;
tic;
y2 = sin(x); % perbaikan: satu panggilan untuk seluruh array
tBaik = toc;
fprintf('sin skalar : %.4f s | sin vector: %.6f s | speedup %.0fx\n', ...
tBuruk, tBaik, tBuruk / tBaik);
fprintf('hasil identik: %d\n\n', isequal(y1, y2));Output yang Dihasilkan:
sin skalar : 0.6507 s | sin vector: 0.002009 s | speedup 324x
hasil identik: 1Mari kita bedah baris-baris kode di atas:
- Baris 5 (
y1 = zeros(1, n);): prealokasi hasil versi loop — kita mengukur biaya pemanggilansin, bukan biaya pertumbuhan array. - Baris 7 (
y1(k) = sin(x(k));): satu iterasi = satu panggilansinuntuk satu skalar; 200000 panggilan berarti 200000 kali dispatch. - Baris 12 (
y2 = sin(x);):sinmenerima array dan memproses seluruh elemen dalam satu panggilan. - Baris 17 (
isequal(y1, y2)): hasil identik — perbaikan ini murni soal kecepatan, bukan nilai.
Konstanta yang Dihitung Ulang #
Aturannya: semua nilai yang tidak berubah selama loop — konstanta matematis, setting, hasil lookup — dihitung sekali sebelum loop. Mekanismenya: ekspresi (1 / sqrt(2 * pi)) di dalam badan loop dievaluasi penuh pada tiap iterasi meskipun hasilnya selalu sama; mesin tidak tahu bahwa pi konstan:
rng(42);
v = rand(1, n);
tic;
acc1 = 0;
for k = 1:n
acc1 = acc1 + exp(v(k)) * (1 / sqrt(2 * pi)); % konstanta dihitung n kali
end
tKonst = toc;
tic;
bobot = 1 / sqrt(2 * pi); % hoist: hitung sekali sebelum loop
acc2 = 0;
for k = 1:n
acc2 = acc2 + exp(v(k)) * bobot;
end
tHoist = toc;
fprintf('konstanta dalam loop: %.4f s | hoist: %.4f s | speedup %.2fx\n', ...
tKonst, tHoist, tKonst / tHoist);
fprintf('selisih relatif: %.2e\n\n', abs(acc1 - acc2) / abs(acc2));Output yang Dihasilkan:
konstanta dalam loop: 0.9911 s | hoist: 0.5468 s | speedup 1.81x
selisih relatif: 0.00e+00Kedua versi tetap memproses exp per elemen (bagian yang sah dari loop), tetapi memangkas sqrt dan pembagian yang sia-sia sudah memotong hampir separuh waktunya. Pola yang sama berlaku untuk lookup mahal: bila lebar = konfigurasi.lebar * skala tidak berubah, hitung di luar.
Indexing Berantai: Cache Handle #
Aturannya: akses a.b.c(i) di dalam loop diganti variabel penampung w = a.b.c; yang diambil sekali sebelum loop. Mekanismenya: setiap data.konfigurasi.bobot(k) menuntaskan dua resolusi field dari struct sebelum elemen k bisa dibaca; dengan cache handle, resolusi itu hanya terjadi sekali:
m = 100000;
data.konfigurasi.bobot = rand(1, m);
tic;
s1 = 0;
for k = 1:m
s1 = s1 + data.konfigurasi.bobot(k); % dua level field tiap iterasi
end
tRantai = toc;
tic;
w = data.konfigurasi.bobot; % cache sekali sebelum loop
s2 = 0;
for k = 1:m
s2 = s2 + w(k);
end
tCache = toc;
fprintf('indexing berantai: %.4f s | cache: %.4f s | speedup %.2fx\n', ...
tRantai, tCache, tRantai / tCache);
fprintf('hasil identik: %d\n\n', isequal(s1, s2));Output yang Dihasilkan:
indexing berantai: 0.2695 s | cache: 0.1352 s | speedup 1.99x
hasil identik: 1Varian pentingnya: method pada objek. obj.hitung(x) dalam loop yang panjang layak dibungkus menjadi function handle f = @(x) obj.hitung(x); sekali di muka, lalu loop memanggil f(x) — rantai resolusi obj.hitung tidak diulang di tiap iterasi.
if yang Tak Pernah Berubah: Hoist #
Aturannya: bila kondisi if tidak berganti selama loop, pindahkan percabangan ke luar dan tulis dua varian badan loop. Mekanismenya: kondisi tetap dievaluasi pada tiap iterasi, dan lebih halus lagi, percabangan yang aktif menghalangi bentuk badan loop yang bisa dioptimalkan lebih jauh (termasuk vectorization):
mode = 1;
arr = rand(1, m);
tic;
z1 = zeros(1, m);
for k = 1:m
if mode == 1
z1(k) = arr(k) * 2; % cabang sama dicek m kali
else
z1(k) = arr(k);
end
end
tIf = toc;
tic;
if mode == 1 % cabang diputus sekali
z2 = arr * 2;
else
z2 = arr;
end
tHoistIf = toc;
fprintf('if dalam loop: %.4f s | hoist+vector: %.6f s | speedup %.0fx\n', ...
tIf, tHoistIf, tIf / tHoistIf);
fprintf('hasil identik: %d\n', isequal(z1, z2));Output yang Dihasilkan:
if dalam loop: 0.2230 s | hoist+vector: 0.000464 s | speedup 481x
hasil identik: 1Dua Anti-Pola Sisanya: Plot dan Panggilan Tiny #
Update plot atau figure di dalam loop adalah anti-pola paling mahal yang paling sering tidak disadari: rendering grafis berbiaya puluhan hingga ribuan kali lipat dari komputasi numerik di sekitarnya. Kumpulkan hasil dulu, gambar sekali setelah loop selesai; bila animasi memang diperlukan, batasi refresh dengan drawnow limitrate di dalam loop agar maksimal satu update per frame.
Pemanggilan fungsi tiny berjuta kali (misalnya kuadrat = @hitungKuadrat yang hanya berisi x.^2) membayar overhead panggilan yang lebih besar dari isi fungsinya. Solusinya: vectorize pemanggilannya (panggil sekali untuk array), atau bila fungsinya sungguh satu baris, tulis ekspresinya langsung di tempat pemanggilan (inlining).
Ringkasan #
Loop yang efisien menjaga pekerjaan mahal di luar badannya: operasi elemen naik ke panggilan vector, konstanta dan lookup dihitung sekali, rantai field di-cache jadi handle, percabangan tetap di-hoist, dan rendering plot ditunda sampai data selesai. Pengukuran pada contoh kita menunjukkan rentang dampaknya — dari 1.8x (konstanta) hingga ratusan kali (kombinasi hoist + vector) — semuanya dengan hasil identik via isequal. Setelah badan loop bersih, biaya terakhir yang tersisa tinggal di memori: chapter berikutnya mengauditnya dengan whos.