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
  • if yang tak pernah berubah → hoist keluar loop
  • Update plot dan pemanggilan fungsi tiny dalam loop

Peta Anti-Pola #

Anti-polaDampakSolusi
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 kalisatu panggilan vector sin(x)
Konstanta dihitung dalam loopekspresi tetap dievaluasi n kalihitung sekali sebelum loop
Indexing berantai data.konfig.bobot(k)resolusi field berulangcache handle sebelum loop
if dengan kondisi tetap dalam loopcabang dicek n kaliputuskan sekali di luar, hoist
Update plot/figure tiap iterasirendering jauh lebih mahal dari komputasibatch / batasi drawnow
Fungsi tiny dipanggil jutaan kalioverhead panggilan menumpukinlining / 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:

matlab
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:

text
sin skalar : 0.6507 s | sin vector: 0.002009 s | speedup 324x
hasil identik: 1

Mari kita bedah baris-baris kode di atas:

  1. Baris 5 (y1 = zeros(1, n);): prealokasi hasil versi loop — kita mengukur biaya pemanggilan sin, bukan biaya pertumbuhan array.
  2. Baris 7 (y1(k) = sin(x(k));): satu iterasi = satu panggilan sin untuk satu skalar; 200000 panggilan berarti 200000 kali dispatch.
  3. Baris 12 (y2 = sin(x);): sin menerima array dan memproses seluruh elemen dalam satu panggilan.
  4. 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:

matlab
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:

text
konstanta dalam loop: 0.9911 s | hoist: 0.5468 s | speedup 1.81x
selisih relatif: 0.00e+00

Kedua 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:

matlab
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:

text
indexing berantai: 0.2695 s | cache: 0.1352 s | speedup 1.99x
hasil identik: 1

Varian 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):

matlab
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:

text
if dalam loop: 0.2230 s | hoist+vector: 0.000464 s | speedup 481x
hasil identik: 1

Dua 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.