Kita baru saja memprofil dan menemukan function yang lambat — sebelum dan sesudah memperbaikinya, kita perlu angka yang bisa dipercaya untuk membandingkan. Mengukur dengan waktu_akhir - waktu_awal sekali jalan saja menghasilkan angka yang berubah-ubah setiap dijalankan, karena derau dari sistem operasi, garbage collector, dan isi cache CPU ikut terukur. Benchmarking adalah disiplin mengukur performa dengan cara yang adil dan berulang; pada Chapter ini kita pelajari alat standarnya di Python: module timeit.
timeit: Alat Pengukur Waktu Standar #
Function timeit.timeit() menjalankan sebuah statement berkali-kali, mematikan garbage collector sementara agar sapuannya tidak mengotori hasil, lalu melaporkan total waktu. Statement yang dibutuhkan persiapan diletakkan di parameter setup yang dieksekusi sekali di awal:
import timeit
waktu_join = timeit.timeit(
stmt="'-'.join(str(n) for n in range(100))",
number=1_000,
)
print(f"join via generator : {waktu_join:.4f} detik untuk 1000 panggilan")
waktu_fstring = timeit.timeit(
stmt="'-'.join([f'{n}' for n in range(100)])",
number=1_000,
)
print(f"join via f-string : {waktu_fstring:.4f} detik untuk 1000 panggilan")Output yang Dihasilkan (waktu bergantung pada mesin):
join via generator : 0.0087 detik untuk 1000 panggilan
join via f-string : 0.0067 detik untuk 1000 panggilanMari kita telaah parameternya:
stmt: statement yang diukur — dijalankan berulang olehtimeit.number=1_000: banyaknya pengulangan; angka ini dipilih agar total durasi cukup besar (kira-kira milidetik ke atas) sehingga derau jam tidak mendominasi.setup(belum dipakai di atas): kode persiapan seperti membuat data — dieksekusi sekali, tidak ikut terukur.
timeit dari Command Line #
Untuk perbandingan cepat di terminal, timeit juga bisa dijalankan sebagai module. Opsi -n menetapkan jumlah loop, -r jumlah pengulangan pengukuran (default 5), dan -s kode setup:
python3 -m timeit -n 100000 -r 5 -s "kata = ['belajar', 'python', 'bersama']" "' '.join(kata)"Output yang Dihasilkan (waktu bergantung pada mesin):
100000 loops, best of 5: 71.1 nsec per loopTanpa -n, timeit menaikkan jumlah loop otomatis sampai total waktu minimal 0,2 detik tercapai:
python3 -m timeit -s "import math" "math.sqrt(144)"Output yang Dihasilkan (waktu bergantung pada mesin):
5000000 loops, best of 5: 59.9 nsec per loopFrasa best of 5 adalah kunci: timeit mengulang pengukuran 5 kali dan melaporkan yang tercepat, bukan rata-rata. Angka terkecil paling sedikit terkontaminasi gangguan eksternal — prinsip yang akan kita bahas lagi di bagian statistik derau.
perf_counter vs time.time #
Untuk benchmark manual di dalam program, pilihan jamnya menentukan kualitas hasil. time.perf_counter() adalah jam beresolusi tinggi yang monotonic — nilainya tidak pernah melompat mundur; sedangkan time.time() adalah jam dinding yang bisa disesuaikan mundur atau maju oleh sinkronisasi NTP. Kedua jam mengukur durasi sleep() dengan benar:
import time
mulai = time.perf_counter()
time.sleep(0.2)
selesai = time.perf_counter()
print(f"perf_counter : {selesai - mulai:.4f} detik")
mulai = time.time()
time.sleep(0.2)
selesai = time.time()
print(f"time.time : {selesai - mulai:.4f} detik")Output yang Dihasilkan:
perf_counter : 0.2001 detik
time.time : 0.2001 detikUntuk durasi, keduanya sama-sama benar di sini. Perbedaannya baru terasa saat jam dinding “berpindah” di tengah pengukuran: hasil time.time() bisa minus atau melonjak palsu, sedangkan perf_counter() dijamin konsisten. Karena itu aturan praktisnya: perf_counter() untuk mengukur durasi, time.time() untuk menampilkan tanggal dan jam kepada manusia — pembahasan mendalam tentang jebakan waktu ada pada Chapter Menangani Waktu dengan Aman.
Membandingkan Dua Implementasi dengan Jujur #
Contoh klasiknya: list.insert(0, ...) versus deque.appendleft(...) untuk menambah elemen di depan. timeit.repeat() menjalankan number loop sebanyak repeat kali dan mengembalikan daftar hasil — persis harness yang kita butuhkan:
import timeit
setup = "data = list(range(10_000))"
waktu_list = timeit.repeat(
stmt="data.insert(0, 'baru')",
setup=setup,
number=1_000,
)
setup = "from collections import deque; data = deque(range(10_000))"
waktu_deque = timeit.repeat(
stmt="data.appendleft('baru')",
setup=setup,
number=1_000,
)
print(f"list.insert(0) : min = {min(waktu_list) * 1000:.2f} ms")
print(f"deque.appendleft: min = {min(waktu_deque) * 1000:.2f} ms")
print(f"list.insert(0) : semua percobaan (ms) = {[round(w * 1000, 2) for w in waktu_list]}")
print(f"deque.appendleft: semua percobaan (ms) = {[round(w * 1000, 2) for w in waktu_deque]}")
print(f"rasio min list/deque = {min(waktu_list) / min(waktu_deque):.0f}x")Output yang Dihasilkan (waktu bergantung pada mesin):
list.insert(0) : min = 1.73 ms
deque.appendleft: min = 0.04 ms
list.insert(0) : semua percobaan (ms) = [1.77, 1.75, 1.74, 1.75, 1.73]
deque.appendleft: semua percobaan (ms) = [0.04, 0.04, 0.04, 0.04, 0.04]
rasio min list/deque = 49xPerhatikan dua hal dari output ini. Pertama, data 10.000 elemen identik di kedua versi, jumlah operasi identik — satu-satunya perbedaan adalah struktur datanya, itulah yang membuat perbandingan jujur. Kedua, kelima percobaan list.insert(0) berkisar 1,73–1,77 ms: derau ±2% itu normal dan justru alasan kita mengulang pengukuran.
Statistik Derau: Kenapa Minimum, Bukan Rata-rata #
Gangguan eksternal — penjadwal process lain, interrupt, cache yang dingin — hanya bisa menambah waktu, tidak pernah menguranginya. Karena itu angka tercepat dari sekian percobaan adalah perkiraan terbaik dari kemampuan sejati kode, sedangkan rata-rata ikut menghitung semua kecelakaan eksternal. Rasio 49x pada output di atas dihitung dari dua nilai minimum; bila dihitung dari rata-rata hasilnya sedikit lebih buruk karena derau menumpuk di kedua sisi.