Kita sudah bisa membaca Big-O dan menemukan hotspot dengan cProfile — sekarang saatnya memperbaiki kode. Pengalaman ribuan programmer menyimpulkan satu hukum yang konsisten: perubahan algoritma hampir selalu mengalahkan micro-optimization. Mengganti loop bersarang dengan dictionary bisa mempercepat program 30x; mengganti x = x + 1 dengan x += 1 tidak akan terlihat di pengukuran mana pun. Pada Chapter ini kita praktikkan tangga optimasi dari yang paling berdampak sampai yang paling akhir dipikirkan.
Temukan Hotspot Dulu, Baru Optimasi #
Aturan pertama optimasi adalah tidak mengoptimasi kode yang tidak lambat. Jalankan cProfile seperti pada Chapter Profiling dengan cProfile, cari satu-dua function dengan tottime terbesar, dan fokus di situ saja. Mempercepat function yang menyumbang 1% waktu program maksimal memperbaiki keseluruhan 1% — sedangkan memperbaiki hotspot 80% langsung terasa. Setelah target jelas, siapkan benchmark sebelum-sesudah dengan timeit agar perbaikannya terukur, bukan terasa.
Kemenangan Algoritmik: O(n²) menjadi O(n) #
Contoh pertama adalah pola yang sangat umum: mencari duplikat dengan membandingkan setiap pasang elemen. Versi pertama memakai loop bersarang O(n²); versi kedua memakai set sebagai memori “sudah pernah terlihat” sehingga cukup satu pass O(n):
import time
def cari_duplikat_list(data):
duplikat = []
for i, nilai in enumerate(data):
for j in range(i + 1, len(data)):
if nilai == data[j]:
if nilai not in duplikat:
duplikat.append(nilai)
return duplikat
def cari_duplikat_set(data):
terlihat = set()
duplikat = []
for nilai in data:
if nilai in terlihat:
if nilai not in duplikat:
duplikat.append(nilai)
else:
terlihat.add(nilai)
return duplikat
data = [i % 500 for i in range(2_000)] # pasti ada duplikat
mulai = time.perf_counter()
hasil_list = cari_duplikat_list(data)
waktu_n2 = time.perf_counter() - mulai
mulai = time.perf_counter()
hasil_set = cari_duplikat_set(data)
waktu_n = time.perf_counter() - mulai
print(f"cari_duplikat_list : {waktu_n2:.4f} detik -> {len(hasil_list)} duplikat")
print(f"cari_duplikat_set : {waktu_n:.6f} detik -> {len(hasil_set)} duplikat")
print(f"hasil sama : {hasil_list == hasil_set}")
print(f"versi set {waktu_n2 / waktu_n:.0f}x lebih cepat")Output yang Dihasilkan (waktu bergantung pada mesin):
cari_duplikat_list : 0.0800 detik -> 500 duplikat
cari_duplikat_set : 0.002175 detik -> 500 duplikat
hasil sama : True
versi set 37x lebih cepatHasilnya 37x lebih cepat pada 2.000 elemen — dan ini bukan percepatan tetap, melainkan yang terus melebar: pada 200.000 elemen versi O(n²) memburuk ratusan kali lipat lagi, sementara versi set hanya tumbuh linear. Bila angka di output Anda sedikit berbeda dari contoh, itu normal — intinya kedua versi menghasilkan duplikat yang sama persis (hasil sama : True), hanya kompleksitasnya yang berbeda.
Rangkaian String: += di dalam Loop vs join() #
Menggabungkan string dengan += di dalam loop adalah anti-pola klasik — setiap penggabungan berpotensi membuat object string baru dan menyalin seluruh isi sebelumnya. Method join() menghitung total panjang sekali di awal lalu menyalin sekali:
import time
KATA = "python"
JUMLAH = 50_000
mulai = time.perf_counter()
hasil_concat = ""
for i in range(JUMLAH):
hasil_concat += KATA
waktu_concat = time.perf_counter() - mulai
mulai = time.perf_counter()
hasil_join = "".join([KATA for _ in range(JUMLAH)])
waktu_join = time.perf_counter() - mulai
print(f"+= di dalam loop : {waktu_concat:.4f} detik (panjang {len(hasil_concat):,})")
print(f"join() : {waktu_join:.4f} detik (panjang {len(hasil_join):,})")
print(f"hasil sama : {hasil_concat == hasil_join}")Output yang Dihasilkan (waktu bergantung pada mesin):
+= di dalam loop : 0.7825 detik (panjang 300,000)
join() : 0.0023 detik (panjang 300,000)
hasil sama : TrueSebenarnya CPython mengandalkan optimasi internal yang membuat += tidak seburuk teori murni — tetapi optimasi itu rapuh: begitu string juga direferensikan di tempat lain, salinan penuh terpaksa dilakukan. join() tidak menyimpan jaminan seperti itu; pola ini selalu cepat, untuk data besar maupun kecil.
Struktur Data yang Tepat dan Prekomputasi #
Kerja yang sama bisa jadi jauh lebih murah hanya karena datanya disusun ulang. Daftar harga dalam format list harus dipindai linear per pencarian; dictionary mencari langsung via hash. Bila daftar harga dipakai berulang kali, konversi sekali di awal (prekomputasi) terbayar cepat:
import time
HARGA = [("beras", 62000), ("gula", 17000), ("teh", 9500),
("kopi", 32000), ("susu", 18500)]
PESANAN = ["teh", "susu", "kopi"]
def total_belanja(nama_barang):
total = 0
for nama in nama_barang:
# mencari di list setiap kali = pencarian linear berulang
for barang, harga in HARGA:
if barang == nama:
total += harga
break
return total
def total_belanja_cepat(nama_barang, daftar_harga):
total = 0
for nama in nama_barang:
total += daftar_harga[nama]
return total
daftar_harga = dict(HARGA) # prekomputasi: konversi sekali di awal
print("hasil versi list :", total_belanja(PESANAN))
print("hasil versi dictionary:", total_belanja_cepat(PESANAN, daftar_harga))
mulai = time.perf_counter()
for _ in range(20_000):
total_belanja(PESANAN)
waktu_list = time.perf_counter() - mulai
mulai = time.perf_counter()
for _ in range(20_000):
total_belanja_cepat(PESANAN, daftar_harga)
waktu_dict = time.perf_counter() - mulai
print(f"versi list : {waktu_list:.4f} detik untuk 20.000 pemanggilan")
print(f"versi dictionary: {waktu_dict:.4f} detik untuk 20.000 pemanggilan")Output yang Dihasilkan (waktu bergantung pada mesin):
hasil versi list : 60000
hasil versi dictionary: 60000
versi list : 0.0162 detik untuk 20.000 pemanggilan
versi dictionary: 0.0066 detik untuk 20.000 pemanggilanPada 20.000 pemanggilan, versi dictionary lebih dari dua kali lebih cepat — dan selisihnya melebar bila daftar barangnya panjang, karena versi list dipindai per item. Pola prekomputasi yang sama berlaku untuk prefix-sum, hasil parsing file konfigurasi, dan hasil query yang dipakai di banyak tempat: hitung sekali, simpan, pakai berulang.
Early Exit: Berhenti Saat Jawaban Sudah Ada #
Loop tidak wajib menyelesaikan seluruh data. Saat jawaban sudah ditemukan, break atau return langsung menghentikan pekerjaan:
import time
data = list(range(10_000_000))
target = 15 # berada tepat di awal data
def cari_tanpa_break(data, target):
"Memindai seluruh list sekalipun target sudah ditemukan."
posisi = None
for i, nilai in enumerate(data):
if nilai == target:
posisi = i
return posisi
def cari_dengan_break(data, target):
"Berhenti begitu target ditemukan."
for i, nilai in enumerate(data):
if nilai == target:
return i
return None
mulai = time.perf_counter()
hasil_1 = cari_tanpa_break(data, target)
waktu_1 = time.perf_counter() - mulai
mulai = time.perf_counter()
hasil_2 = cari_dengan_break(data, target)
waktu_2 = time.perf_counter() - mulai
print(f"tanpa break : {waktu_1:.4f} detik, posisi = {hasil_1}")
print(f"dengan break : {waktu_2:.8f} detik, posisi = {hasil_2}")Output yang Dihasilkan (waktu bergantung pada mesin):
tanpa break : 0.5137 detik, posisi = 15
dengan break : 0.00001290 detik, posisi = 15Sepuluh juta elemen dipindai 0,5 detik; berhenti di elemen ke-16 hanya butuh 13 mikrosekon. any(), all(), dan in juga sudah menerapkan early exit secara bawaan.
Tangga Optimasi dan Posisi Micro-Optimization #
Semua contoh di atas bisa diringkas jadi tangga prioritas — naik dari bawah ke atas, jangan melompat:
| Prioritas | Langkah | Contoh di chapter ini | Dampak tipikal |
|---|---|---|---|
| 1 | Perbaiki algoritma / kompleksitas | O(n²) → set | 10x – 1000x |
| 2 | Ganti struktur data | list → dict untuk lookup | 2x – 10x |
| 3 | Kurangi kerja berulang | prekomputasi, join(), early exit | 2x – 100x |
| 4 | Caching hasil | Chapter Caching | tergantung pola |
| 5 | Paralelisme | multiprocessing untuk CPU-bound | mendekati jumlah core |
| 6 | Micro-optimization | menghindari attribute lookup | 1,1x – 2x |
Aturan penutupnya sederhana: setiap optimasi harus dibuktikan dengan angka benchmark sebelum-sesudah. Bila angkanya tidak membaik, kembalikan kode yang lebih mudah dibaca.