Dua fungsi NumPy bisa mengembalikan hasil yang sama persis, tetapi salah satunya selesai dalam hitungan mikrodetik sementara yang lain butuh detik. Sebelum mengubah kode demi kecepatan, kita butuh alat ukur yang andal — tanpa pengukuran, optimasi hanya berupa tebakan, dan tebakan sering salah arah.

Bab ini memperkenalkan dua alat bawaan Python: time.perf_counter untuk mencatat durasi satu potongan kode secara manual, dan modul timeit untuk mengukur berulang kali secara adil. Kita juga meninjau jebakan yang membuat angka pengukuran menyesatkan: efek warm-up, array yang terlalu kecil, serta perbedaan jam wall-clock dan jam CPU.

Mengukur dengan time.perf_counter #

time.perf_counter() mengembalikan nilai jam dengan resolusi tertinggi yang tersedia di sistem. Pola pemakaiannya sederhana: catat jam sebelum dan sesudah pekerjaan, lalu selisih keduanya adalah durasi.

python
# notest
import numpy as np
import time

x = np.arange(1_000_000, dtype=np.float64)
mulai = time.perf_counter()
total = np.sum(x)
selesai = time.perf_counter()
print(f"Durasi np.sum: {(selesai - mulai) * 1000:.3f} ms")
print(f"Total: {total:.1f}")

Output yang Dihasilkan:

text
# (ilustrasi — angka waktu berbeda di tiap komputer)
Durasi np.sum: 0.613 ms
Total: 499999500000.0

Bedah baris demi baris:

  1. Baris 5 (x = np.arange(...)): array satu juta elemen float64 disiapkan sebagai bahan uji; ukuran ini cukup besar agar durasi operasinya terasa di atas overhead pencatatan jam.
  2. Baris 6-8: jam dicatat tepat sebelum dan sesudah np.sum(x), sehingga yang terukur hanya pekerjaan di antara kedua catatan.
  3. Baris 9-10 (print(...)): selisih jam berumur dalam detik, maka dikalikan 1000 dan diformat tiga desimal agar terbaca sebagai milidetik.

Memilih Jam yang Tepat #

Python menyediakan beberapa jam, dan memilih yang salah bisa membuat pengukuran bias. Kita dapat memeriksa sifat tiap jam lewat time.get_clock_info() tanpa perlu mengukur apa pun, sehingga output berikut deterministik:

python
import numpy as np
import time

for nama in ("perf_counter", "process_time", "time"):
    info = time.get_clock_info(nama)
    print(f"{nama}: monotonic={info.monotonic}, adjustable={info.adjustable}")

Output yang Dihasilkan:

text
perf_counter: monotonic=True, adjustable=False
process_time: monotonic=True, adjustable=False
time: monotonic=False, adjustable=True

Cara membaca hasilnya:

  1. perf_counter: monotonic (tidak pernah mundur) dan tidak dapat disetel sistem — pasangan sifat inilah yang dicari untuk mengukur durasi.
  2. process_time: juga monotonic, tetapi hanya menghitung waktu CPU proses kita sendiri; waktu menunggu I/O atau tidur tidak terhitung.
  3. time: jam wall-clock yang dapat disetel sistem (adjustable True), sehingga nilainya bisa melompat — layak untuk mencatat waktu kejadian, bukan untuk mengukur durasi.
JamMonotonicMengukurCocok untuk
time.perf_counterYawaktu riil yang berjalanbenchmark durasi kode
time.process_timeYakerja CPU proses inikomputasi murni tanpa I/O
time.timeTidakwall-clock sistemstempel waktu kejadian

timeit untuk Pengukuran yang Adil #

Satu kali pengukuran mudah terdistorsi oleh gangguan sesaat. Modul timeit mengeksekusi kode berkali-kali (argumen number) dan mengulangi sesi tersebut beberapa kali (argumen repeat), sehingga kita dapat mengambil hasil terbaik. Kebiasaan yang lazim: bandingkan versi lambat dan cepat pada data yang sama, lalu laporkan rasionya.

python
# notest
import numpy as np
import timeit

x = np.arange(100_000, dtype=np.float64)

def dengan_loop():
    total = 0.0
    for nilai in x:
        total += nilai
    return total

def dengan_numpy():
    return np.sum(x)

t_loop = min(timeit.repeat(dengan_loop, number=1, repeat=5))
t_numpy = min(timeit.repeat(dengan_numpy, number=1000, repeat=5)) / 1000
print(f"Loop Python : {t_loop * 1000:.2f} ms")
print(f"np.sum      : {t_numpy * 1000:.4f} ms")
print(f"Rasio       : {t_loop / t_numpy:.0f}x")

Output yang Dihasilkan:

text
# (ilustrasi — angka waktu berbeda di tiap komputer)
Loop Python : 6.07 ms
np.sum      : 0.0190 ms
Rasio       : 320x

Bedah baris demi baris:

  1. Baris 7-11 (dengan_loop): penjumlahan elemen demi elemen dari array NumPy; setiap iterasi membuka objek scalar Python baru, dan inilah sumber kelebihan biayanya.
  2. Baris 16 (timeit.repeat(dengan_loop, number=1, repeat=5)): fungsi lambat diukur lima sesi, masing-masing satu eksekusi, karena satu eksekusinya sudah cukup lama.
  3. Baris 17: np.sum sangat cepat, maka diukur seribu eksekusi per sesi lalu dibagi 1000 supaya satuannya sebanding dengan t_loop.
  4. min(...): hasil terbaik dari lima sesi diambil karena gangguan sistem operasi hanya bisa menambah waktu, tidak pernah menguranginya.

Jebakan yang Membuat Angka Menyesatkan #

Beberapa kebiasaan membuat hasil benchmark tidak dapat dipercaya:

  1. Mengabaikan warm-up: eksekusi pertama biasanya lebih lambat karena data belum berada di cache prosesor dan modul baru saja diimpor. Buang hasil percobaan pertama atau gunakan timeit yang mengulang secara otomatis.
  2. Array terlalu kecil: pada array sepuluh elemen, biaya pemanggilan fungsi lebih besar daripada pekerjaannya, sehingga hampir semua waktu terpakai untuk overhead — bukan untuk operasi NumPy.
  3. Satu kali ukur, langsung simpulkan: ambil nilai terbaik dari beberapa repeat, atau laporkan rentangnya.
  4. Mencetak di dalam kode yang diukur: print menulis ke layar dan jauh lebih lambat daripada operasi array; pisahkan pencatatan hasil dari bagian yang diukur.
  5. Mencampur jam wall-clock dan CPU: perf_counter mengukur waktu riil termasuk waktu menunggu; jika mesin sedang sibuk oleh proses lain, process_time memberi gambaran kerja CPU yang lebih murni.

Setelah alat ukurnya siap, pertanyaan berikutnya adalah mengapa dua array berukuran sama bisa berbeda kecepatan. Jawabannya ada pada cara data tersusun di memori — topik Chapter Memahami Memory Layout.