Dengan rancangan yang terkunci, kita merekam baseline: berapa lama ketiga fungsi loop menyelesaikan tugasnya. Satu hal perlu disepakati lebih dulu — angka waktu di bagian ini adalah contoh hasil dari komputer penulis dan pasti berbeda di tiap komputer. Yang bisa ditiru siapa pun adalah cara mengukurnya dan cara membacanya.

Menyiapkan timeit.repeat #

timeit menjalankan pernyataan sebanyak number kali dan mengukur total durasinya; timeit.repeat mengulangi proses itu sebanyak repeat kali. Struktur hasilnya bisa kita periksa tanpa mencetak satu pun angka waktu:

python
import numpy as np
import timeit

rng = np.random.default_rng(42)
data = rng.random(1_000)

def jumlah_loop(data):
    total = 0.0
    for nilai in data:
        total += nilai
    return total

hasil = timeit.repeat(lambda: jumlah_loop(data), number=1, repeat=3)
print(type(hasil).__name__)
print(len(hasil))
print(all(t > 0 for t in hasil))

Output yang Dihasilkan:

text
list
3
True

Mari kita bedah:

  1. Baris 10 (lambda: jumlah_loop(data)): timeit menerima callable tanpa argumen, sehingga lambda menutup array data untuk fungsi yang diukur.
  2. Baris 10 (number=1, repeat=3): satu eksekusi per pengukuran, diulang tiga kali; hasilnya list berisi tiga durasi float dalam satuan detik.
  3. Baris 11-13: kita hanya mencetak tipe, panjang, dan tanda bahwa semua durasi positif — struktur pengukuran terverifikasi tanpa satu angka waktu pun masuk output teruji.

Mengapa angka waktu dilarang tampil di blok teruji? Karena nilainya berubah antar mesin, antar sesi, bahkan antar detik pada mesin yang sama. Blok pengukuran sesungguhnya di bawah ini karenanya diberi tanda # notest: kodenya nyata dan bisa dijalankan, outputnya hanya contoh.

Mengukur Ketiga Versi Loop #

python
# notest
import numpy as np
import timeit

rng = np.random.default_rng(42)
data = rng.random(1_000_000)
bobot = rng.random(1_000_000)

def jumlah_loop(data):
    total = 0.0
    for nilai in data:
        total += nilai
    return total

def dot_loop(a, b):
    total = 0.0
    for i in range(len(a)):
        total += a[i] * b[i]
    return total

def filter_loop(data, ambang):
    hasil = []
    for nilai in data:
        if nilai > ambang:
            hasil.append(nilai)
    return np.array(hasil)

for nama, aksi in [
    ("jumlah_loop", lambda: jumlah_loop(data)),
    ("dot_loop", lambda: dot_loop(data, bobot)),
    ("filter_loop", lambda: filter_loop(data, 0.5)),
]:
    waktu = timeit.repeat(aksi, number=1, repeat=5)
    print(f"{nama}: {[f'{t * 1000:.1f} ms' for t in waktu]}")

Output yang Dihasilkan (contoh — angka waktu berbeda di tiap komputer):

text
jumlah_loop: ['65.6 ms', '83.3 ms', '83.3 ms', '82.2 ms', '83.2 ms']
dot_loop: ['226.7 ms', '216.5 ms', '214.4 ms', '238.8 ms', '221.2 ms']
filter_loop: ['137.5 ms', '141.7 ms', '110.2 ms', '110.6 ms', '109.3 ms']

Kita bedah kodenya — bukan angkanya:

  1. Baris 22-26 (daftar aksi): ketiga fungsi dibungkus lambda agar timeit memanggilnya tanpa argumen; nama skenario disertakan supaya laporan terbaca.
  2. Baris 28 (timeit.repeat(aksi, number=1, repeat=5)): lima durasi per fungsi; format f"{t * 1000:.1f} ms" menerjemahkan detik menjadi milidetik satu desimal.
  3. Baris 29 (print(...)): seluruh list durasi dicetak utuh, karena pola naik-turunnya justru informasi yang akan kita bedah pada bagian berikutnya.

Pada contoh di atas, jumlah_loop butuh sekitar 65-83 ms, dot_loop sekitar 214-239 ms, dan filter_loop sekitar 109-142 ms untuk satu juta elemen. Ketiganya ratusan ribu kali lebih lambat daripada satu operasi biasa — dan sepuluh hingga ratusan kali lebih lambat daripada lawan vectorized-nya yang akan kita ukur di bagian ketiga.

Membaca Variasi antar Pengulangan #

Lima angka per fungsi bukan pemborosan; sebarannya adalah data. Cara ringkas membacanya adalah membandingkan nilai tercepat, median, dan terlambat:

python
# notest
import numpy as np
import timeit

rng = np.random.default_rng(42)
data = rng.random(1_000_000)

def jumlah_loop(data):
    total = 0.0
    for nilai in data:
        total += nilai
    return total

waktu = timeit.repeat(lambda: jumlah_loop(data), number=1, repeat=7)
print(f"tercepat : {min(waktu) * 1000:.1f} ms")
print(f"median   : {sorted(waktu)[3] * 1000:.1f} ms")
print(f"terlambat: {max(waktu) * 1000:.1f} ms")

Output yang Dihasilkan (contoh — angka waktu berbeda di tiap komputer):

text
tercepat : 60.9 ms
median   : 61.2 ms
terlambat: 66.5 ms

Pembacaannya: nilai tercepat paling dekat dengan biaya sesungguhnya karena hampir tak terganggu apa pun; median mewakili kondisi khas; nilai terlambat menangkap gangguan sesaat. Selisih ketiganya pada contoh ini hanya sekitar 10 persen — sinyal bahwa mesin sedang tenang. Selisih yang jauh lebih besar berarti ada gangguan dan pengukuran perlu diulang.

Sumber gangguan yang paling sering ditemui:

  1. Warm-up: eksekusi pertama lebih lambat karena cache CPU dan data masih dingin; jalankan sekali tanpa dicatat sebelum serangkaian pengukuran.
  2. Garbage collector: timeit mematikan GC selama pengukuran, sedangkan program sungguhan tidak — hasil bisa sedikit terlalu optimis.
  3. CPU throttling: mesin yang panas menurunkan frekuensinya; urutan pengukuran ikut memengaruhi hasil.
  4. Proses latar: browser, sinkronisasi berkas, atau indeks sistem menambah noise di sela eksekusi.
  5. Repeat terlalu sedikit: satu pengukuran tidak bisa membedakan sinyal dari kebetulan.

Baseline sudah tercatat. Bagian berikutnya mengukur lawannya — versi vectorized.