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

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

text
cari_duplikat_list : 0.0800 detik -> 500 duplikat
cari_duplikat_set  : 0.002175 detik -> 500 duplikat
hasil sama         : True
versi set 37x lebih cepat

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

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

text
+= di dalam loop : 0.7825 detik (panjang 300,000)
join()           : 0.0023 detik (panjang 300,000)
hasil sama       : True

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

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

text
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 pemanggilan

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

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

text
tanpa break : 0.5137 detik, posisi = 15
dengan break : 0.00001290 detik, posisi = 15

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

PrioritasLangkahContoh di chapter iniDampak tipikal
1Perbaiki algoritma / kompleksitasO(n²) → set10x – 1000x
2Ganti struktur datalist → dict untuk lookup2x – 10x
3Kurangi kerja berulangprekomputasi, join(), early exit2x – 100x
4Caching hasilChapter Cachingtergantung pola
5Paralelismemultiprocessing untuk CPU-boundmendekati jumlah core
6Micro-optimizationmenghindari attribute lookup1,1x – 2x

Aturan penutupnya sederhana: setiap optimasi harus dibuktikan dengan angka benchmark sebelum-sesudah. Bila angkanya tidak membaik, kembalikan kode yang lebih mudah dibaca.