Profiling dan benchmarking

Mengukur kinerja dengan timeit dan cProfile sebelum mengoptimasi.

Daftar isi

Optimasi yang dilakukan tanpa pengukuran adalah tebakan yang mahal. Topik ini membahas dua alat pustaka standar untuk mengganti tebakan dengan angka, yaitu timeit untuk mengukur ekspresi kecil dan cProfile untuk menemukan bagian program yang paling banyak menghabiskan waktu. Keduanya dibahas bersama cara membaca keluarannya, serta pemilihan jam (clock) yang tepat ketika pengukuran ditulis sendiri. Prinsip yang dipakai sepanjang topik ini berlaku umum, yaitu mengukur dahulu, baru melakukan optimasi.

timeit untuk pengukuran mikro#

Modul timeit menjalankan potongan kode berkali-kali lalu melaporkan waktu terbaiknya, dengan pengumpul sampah (garbage collector) dimatikan selama pengukuran agar hasilnya tidak terganggu. Angka absolutnya bervariasi antar mesin dan antar sesi, sehingga nilai yang bermakna adalah perbandingan antar varian yang diukur pada kondisi yang sama. Contoh berikut menyiapkan kode yang akan diukur dan menunjukkan hasil fungsionalnya.

python Contoh: kode yang akan diukur
def gabung(n):
    return "-".join(str(i) for i in range(n))

print(gabung(10))
text Keluaran
0-1-2-3-4-5-6-7-8-9

Fungsi gabung() berjalan dengan benar dan menghasilkan urutan angka yang diharapkan, sehingga pengukurannya layak dilakukan. Pengukuran itu sendiri dilakukan melalui antarmuka fungsi atau antarmuka CLI.

python Contoh: pengukuran dengan timeit
# tidak-dijalankan
import timeit

waktu = timeit.timeit("gabung(100)", globals=globals(), number=10000)
print(waktu)

Argumen number menentukan berapa kali potongan kode diulang, sedangkan globals=globals() membuat string yang diukur dapat membaca fungsi gabung() dari contoh sebelumnya. Fungsi timeit() mengembalikan total waktu dalam detik berupa bilangan pecahan yang berbeda pada setiap sesi, sehingga blok ini tidak disertai keluaran yang ditangkap. Alternatif CLI-nya bahkan lebih singkat.

bash Contoh: pengukuran lewat CLI timeit
# tidak-dijalankan
python3 -m timeit "'-'.join(str(n) for n in range(100))"

CLI mencetak baris seperti 50000 loops, best of 5: 8.35 usec per loop, yaitu jumlah putaran yang dipilih otomatis, waktu terbaik dari lima kali pengulangan, dan waktu per putaran. Angkanya berubah antar mesin, tetapi urutan besarannya cukup untuk membandingkan dua varian secara adil sepanjang keduanya diukur pada kondisi yang sama.

cProfile untuk mencari titik panas#

Modul cProfile mencatat setiap pemanggilan fungsi beserta waktunya, teknik yang disebut deterministic profiling. Hasilnya berupa statistik per fungsi, yaitu jumlah panggilan, waktu di dalam fungsi sendiri, waktu termasuk subpanggilannya, dan lokasi sumbernya. Contoh berikut memprofilkan program kecil yang memanggil satu fungsi berkali-kali.

python Contoh: memprofilkan fungsi utama
# tidak-dijalankan
import cProfile
import pstats

def jumlah_kuadrat(n):
    total = 0
    for i in range(n):
        total += i * i
    return total

def utama():
    return sum(jumlah_kuadrat(i) for i in range(200))

profil = cProfile.Profile()
profil.enable()
utama()
profil.disable()
pstats.Stats(profil).sort_stats("cumulative").print_stats(5)

Blok ini tidak dijalankan di lingkungan penulisan karena keluarannya memuat angka waktu yang berbeda pada setiap sesi. Bentuk keluarannya tetap dapat dijelaskan. Bagian atasnya menyatakan total jumlah pemanggilan fungsi dan total waktu, lalu tabel berikutnya berkolom ncalls (jumlah panggilan), tottime (waktu di dalam fungsi tanpa subpanggilan), cumtime (waktu termasuk subpanggilan), percall, serta lokasi sumber. Pada program ini, jumlah_kuadrat dipanggil 200 kali dan menyerap sebagian besar waktu, sementara utama dan sum masing-masing satu kali.

Menjalankan profiler menambah biaya, sehingga angka absolutnya menyesatkan dan hanya urutan relatifnya yang dipakai. Praktik yang lazim adalah memprofilkan beban kerja yang mewakili pemakaian sebenarnya, membaca tabelnya dari atas, lalu menjadikan satu fungsi teratas sebagai sasaran pengukuran lanjutan dengan timeit.

Membaca tottime dan cumtime#

Dua kolom itu menjawab pertanyaan yang berbeda. Pengurutan dengan sort_stats("cumulative") menampilkan rantai panggilan yang paling berat secara keseluruhan, sehingga cocok untuk memahami alur program. Pengurutan dengan sort_stats("tottime") menampilkan fungsi yang sendirian paling mahal, sehingga cocok untuk menentukan sasaran perbaikan. Fungsi pembungkus tipis kerap menunjukkan cumtime besar tetapi tottime kecil, yang berarti biayanya justru berada di subpanggilannya.

Setelah titik panas ditemukan, perbaikan algoritmik biasanya memberi dampak lebih besar daripada penyetelan mikro, misalnya mengurangi jumlah pemanggilan atau mengganti struktur datanya. Perubahan lalu diukur ulang dengan timeit, dan siklus ukur-ubah-ukur ini diulang sampai perbaikan berikutnya tidak lagi sepadan dengan kerumitannya.

Tabel yang sama juga menunjukkan jumlah panggilan, dan angka ini kerap menjadi petunjuk yang paling murah. Fungsi yang dipanggil ribuan kali dengan isi kecil biasanya lebih mudah diperbaiki dengan mengurangi jumlah panggilannya daripada mempercepat isinya. Dengan demikian, kombinasi kedua kolom dan jumlah panggilan cukup untuk menentukan langkah pertama tanpa alat tambahan.

Jam yang tepat untuk pengukuran sendiri#

Ketika pengukuran ditulis sendiri dengan time, pilihan jamnya menentukan mutu hasil. Fungsi time.time() adalah jam dinding (wall clock) yang dapat digeser oleh sistem, misalnya oleh sinkronisasi waktu, sedangkan time.perf_counter() adalah jam beresolusi tinggi yang monoton, sehingga tepat untuk mengukur durasi. Fungsi time.get_clock_info() melaporkan sifat keduanya secara langsung.

python Contoh: sifat jam perf_counter dan time
import time

info = time.get_clock_info("perf_counter")
print(info.monotonic)
print(info.adjustable)
info_dinding = time.get_clock_info("time")
print(info_dinding.monotonic)
text Keluaran
True
False
False

Jam perf_counter monoton dan tidak dapat disetel, sedangkan jam time tidak monoton karena dapat digeser oleh sistem. Untuk durasi, perf_counter-lah yang dipakai, dan timeit pun mengandalkan jam semacam ini di dalamnya. Pola pengukuran yang lazim adalah mengambil perf_counter() sebelum dan sesudah pekerjaan, kemudian mengurangkannya, dengan pengulangan beberapa kali untuk mengurangi variansi. Pola sederhana ini cukup untuk keputusan cepat, sedangkan pengukuran yang serius tetap diserahkan kepada timeit yang menangani pengulangan dan pemilihan waktu terbaiknya. Pemilihan jam juga menjadi alasan angka benchmark lintas mesin tidak dibandingkan mentah-mentah.

Kesalahan umum#

Kesalahan pertama adalah mengoptimasi tanpa mengukur. Titik panas sering berada di tempat yang tidak diperkirakan, dan perubahan yang tampak cerdas dapat memperlambat program atau merusak keterbacaan tanpa keuntungan yang terukur. Alur yang sehat adalah mengukur, menemukan titik panas, mengubah satu hal, lalu mengukur kembali agar setiap perubahan punya bukti.

Kesalahan kedua adalah mengandalkan pengukuran sekali jalan. Waktu satu eksekusi dipengaruhi pemanasan cache, garbage collection, dan penjadwalan sistem operasi, sehingga angkanya melompat dari satu sesi ke sesi lain. Modul timeit mengurangi variansinya dengan nilai number yang cukup dan waktu terbaik dari lima kali pengulangan, sedangkan cProfile cukup dijalankan pada beban kerja yang mewakili pemakaian sebenarnya, bukan pada data yang terlalu kecil.

Ringkasan#

  • timeit mengukur ekspresi kecil dengan banyak putaran dan pengumpul sampah yang dimatikan, dan nilainya dibaca sebagai perbandingan antar varian, bukan angka mutlak.
  • cProfile mencatat statistik per fungsi, dengan tottime untuk biaya sendiri dan cumtime untuk biaya beserta subpanggilannya.
  • Keluaran pengukuran berisi angka yang berubah antar sesi, sedangkan jumlah panggilan dan urutan relatifnya stabil dan justru itu petunjuk pertama.
  • Pengukuran manual memakai time.perf_counter yang monoton, bukan time.time yang dapat digeser sistem.

Referensi#