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:

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

text
join via generator : 0.0087 detik untuk 1000 panggilan
join via f-string : 0.0067 detik untuk 1000 panggilan

Mari kita telaah parameternya:

  1. stmt: statement yang diukur — dijalankan berulang oleh timeit.
  2. number=1_000: banyaknya pengulangan; angka ini dipilih agar total durasi cukup besar (kira-kira milidetik ke atas) sehingga derau jam tidak mendominasi.
  3. 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:

bash
python3 -m timeit -n 100000 -r 5 -s "kata = ['belajar', 'python', 'bersama']" "' '.join(kata)"

Output yang Dihasilkan (waktu bergantung pada mesin):

text
100000 loops, best of 5: 71.1 nsec per loop

Tanpa -n, timeit menaikkan jumlah loop otomatis sampai total waktu minimal 0,2 detik tercapai:

bash
python3 -m timeit -s "import math" "math.sqrt(144)"

Output yang Dihasilkan (waktu bergantung pada mesin):

text
5000000 loops, best of 5: 59.9 nsec per loop

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

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

text
perf_counter : 0.2001 detik
time.time    : 0.2001 detik

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

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

text
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 = 49x

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