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.
# 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:
# (ilustrasi — angka waktu berbeda di tiap komputer)
Durasi np.sum: 0.613 ms
Total: 499999500000.0Bedah baris demi baris:
- Baris 5 (
x = np.arange(...)): array satu juta elemenfloat64disiapkan sebagai bahan uji; ukuran ini cukup besar agar durasi operasinya terasa di atas overhead pencatatan jam. - Baris 6-8: jam dicatat tepat sebelum dan sesudah
np.sum(x), sehingga yang terukur hanya pekerjaan di antara kedua catatan. - 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:
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:
perf_counter: monotonic=True, adjustable=False
process_time: monotonic=True, adjustable=False
time: monotonic=False, adjustable=TrueCara membaca hasilnya:
perf_counter: monotonic (tidak pernah mundur) dan tidak dapat disetel sistem — pasangan sifat inilah yang dicari untuk mengukur durasi.process_time: juga monotonic, tetapi hanya menghitung waktu CPU proses kita sendiri; waktu menunggu I/O atau tidur tidak terhitung.time: jam wall-clock yang dapat disetel sistem (adjustable True), sehingga nilainya bisa melompat — layak untuk mencatat waktu kejadian, bukan untuk mengukur durasi.
| Jam | Monotonic | Mengukur | Cocok untuk |
|---|---|---|---|
time.perf_counter | Ya | waktu riil yang berjalan | benchmark durasi kode |
time.process_time | Ya | kerja CPU proses ini | komputasi murni tanpa I/O |
time.time | Tidak | wall-clock sistem | stempel 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.
# 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:
# (ilustrasi — angka waktu berbeda di tiap komputer)
Loop Python : 6.07 ms
np.sum : 0.0190 ms
Rasio : 320xBedah baris demi baris:
- Baris 7-11 (
dengan_loop): penjumlahan elemen demi elemen dari array NumPy; setiap iterasi membuka objek scalar Python baru, dan inilah sumber kelebihan biayanya. - 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. - Baris 17:
np.sumsangat cepat, maka diukur seribu eksekusi per sesi lalu dibagi1000supaya satuannya sebanding dengant_loop. 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:
- 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
timeityang mengulang secara otomatis. - 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.
- Satu kali ukur, langsung simpulkan: ambil nilai terbaik dari beberapa
repeat, atau laporkan rentangnya. - Mencetak di dalam kode yang diukur:
printmenulis ke layar dan jauh lebih lambat daripada operasi array; pisahkan pencatatan hasil dari bagian yang diukur. - Mencampur jam wall-clock dan CPU:
perf_countermengukur waktu riil termasuk waktu menunggu; jika mesin sedang sibuk oleh proses lain,process_timememberi 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.