JIT Compiler Python Akhirnya Datang
Python 3.15 membawa JIT compiler copy-and-patch. Cara kerjanya, speedup yang realistis, dan alasan CPython butuh waktu selama ini untuk sampai ke sini.

Python itu 'terlalu lambat' selama Python ada. Jawaban standar dari komunitas Python — 'pakai ekstensi C untuk loop yang panas' — selalu menjadi pengakuan bahwa model eksekusi default bahasa ini memang punya batas mendasar. CPython menginterpretasikan bytecode satu instruksi per satu, dengan setiap instruksi dikirim lewat sebuah switch statement. Sederhana, portabel, dan gampang di-debug. Tapi untuk pekerjaan yang berat secara komputasi, kecepatannya sekitar 100x lebih lambat dibanding C yang dikompilasi.
Python 3.15 mengubah keadaan ini. Setelah bertahun-tahun eksperimen, JIT compiler copy-and-patch akhirnya dirilis sebagai fitur yang aktif secara default. Ini memang tidak akan membuat Python secepat C — tidak ada yang bisa, kecuali kompilasi statis — tapi benchmark awal menunjukkan peningkatan kecepatan 15-30% pada kode dunia nyata, dengan pola tertentu yang mendapat peningkatan jauh lebih besar. Untuk bahasa yang performanya selama 30 tahun terakhir identik dengan 'tulis ulang bagian yang panas dalam C', JIT yang benar-benar mempercepat kode Python murni adalah tonggak yang nyata.
Kenapa CPython Tidak Pernah Punya JIT
Bukan karena tidak ada yang mencoba. PyPy sudah punya JIT selama lebih dari satu dekade dan rutin menjalankan kode Python 5-10x lebih cepat dibanding CPython. Tapi PyPy adalah implementasi terpisah dengan runtime sendiri, dan ia tidak pernah menyaingi pangsa pasar CPython karena ekosistem ekstensi C — NumPy, pandas, scikit-learn, semua yang membuat Python jadi bahasa utama untuk data science — terikat pada C API milik CPython.
Memasukkan JIT ke dalam CPython sendiri sudah sering dibahas dan dicoba. Tantangannya sudah terdokumentasi dengan baik. Arsitektur CPython menyulitkan kompilasi JIT: bytecode-nya bertipe dinamis (JIT butuh informasi tipe untuk menghasilkan kode yang efisien, sementara Python tidak menyediakannya secara statis), C API memungkinkan kode C memanipulasi objek Python secara langsung dengan cara yang melanggar asumsi JIT, dan garbage collector reference counting pada interpreter menimbulkan overhead pembukuan yang tidak mudah dihilangkan oleh JIT.
Upaya sebelumnya — Unladen Swallow (Google, 2009), Pyston (Dropbox, 2014) — mencoba menempelkan kompilasi JIT berbasis LLVM ke CPython. Keduanya menemukan bahwa overhead kompilasi LLVM terlalu tinggi untuk beban kerja Python yang umum. LLVM dirancang untuk kompilasi ahead-of-time pada basis kode besar; memakainya untuk JIT-compile fungsi Python yang pendek menambah milidetik waktu kompilasi untuk eksekusi yang hanya mikrodetik. Biaya kompilasinya malah lebih mahal daripada speedup yang didapat.
Copy-and-Patch: Jenis JIT yang Berbeda
Teknik copy-and-patch, yang diperkenalkan dalam sebuah paper riset pada 2021, mengambil pendekatan yang secara fundamental berbeda untuk kompilasi JIT. Alih-alih menerjemahkan bytecode ke representasi perantara dan menjalankan passes optimisasi (pendekatan LLVM), copy-and-patch bekerja dengan template kode yang sudah dikompilasi sebelumnya.
Idenya begini: untuk setiap instruksi bytecode (LOAD_FAST, BINARY_ADD, CALL_FUNCTION, dan seterusnya), compiler mengompilasi implementasi C-nya menjadi machine code di awal, dengan 'lubang' placeholder untuk data spesifik variabel — alokasi register, nilai konstanta, offset memori. Saat runtime, JIT compilation cuma berisi: salin template yang sudah jadi, lalu isi lubangnya dengan nilai spesifik untuk fungsi ini. Tanpa optimization pass, tanpa algoritma register allocation, tanpa instruction selection. Cukup memcpy dan patch.
Traditional JIT pipeline:
Python bytecode
→ Parse into IR
→ Type inference
→ Optimization passes (CSE, DCE, loop unrolling, inlining...)
→ Register allocation
→ Instruction selection
→ Machine code
Total: 1-100ms per function
Copy-and-patch pipeline:
Python bytecode
→ For each instruction, copy pre-compiled template
→ Fill in holes (constants, offsets)
→ Done — machine code ready
Total: 1-100μs per function (1000x faster compilation)
Trade-off-nya ada di kualitas kode. LLVM menghasilkan machine code yang sangat teroptimasi. Copy-and-patch menghasilkan machine code yang pada dasarnya adalah versi terkompilasi dari interpreter loop — setiap instruksi bytecode tetap menjadi template terpisah, dengan optimisasi lintas-instruksi yang minimal. Kode yang dihasilkan lebih baik dibanding interpretasi (tidak ada overhead dispatch, tidak ada switch statement, prediksi cabang lebih baik) tapi lebih buruk dibanding hasil dari optimizing compiler penuh.
Untuk Python, trade-off ini sangat pas. Fungsi Python biasanya pendek, sering dipanggil, dan masing-masing hanya makan mikrodetik. JIT yang kompilasinya selesai dalam mikrodetik dan mempercepat eksekusi 20-30% lebih berharga dibanding yang kompilasinya memakan milidetik tapi mempercepat eksekusi 200% — karena overhead kompilasi pada pendekatan pertama langsung tertutupi hampir seketika.
Apa yang Jadi Lebih Cepat
JIT tidak serta-merta mempercepat semua kode Python secara merata. Untuk tahu apa yang paling diuntungkan, kita perlu paham dulu apa yang menghabiskan waktu interpreter.
Overhead dispatch bytecode. Di interpreter, setiap instruksi bytecode butuh: ambil opcode berikutnya, decode, lalu lompat ke handler lewat switch statement. Overhead dispatch ini bisa mencapai 30-50% dari total waktu eksekusi pada loop yang ketat. JIT menghilangkannya sepenuhnya — instruksi dikompilasi menjadi machine code berurutan dengan lompatan langsung.
Operasi yang dispesialisasi tipe. Python 3.11 memperkenalkan specializing adaptive interpreter, yang mengganti operasi generik dengan versi khusus tipe setelah mengamati tipe yang benar-benar dipakai. BINARY_ADD berubah menjadi BINARY_ADD_INT saat melihat dua integer. JIT mengompilasi instruksi spesialisasi ini menjadi machine code yang efisien — penjumlahan integer menjadi satu instruksi add, bukan pemanggilan fungsi.
Prediksi cabang. Dispatch loop pusat pada interpreter — sebuah switch dengan ratusan case — adalah mimpi buruk bagi branch predictor CPU. JIT menggantinya dengan alur kontrol langsung yang bisa diprediksi CPU dengan akurat. Pada CPU modern di mana salah prediksi cabang memakan 15-20 siklus, faktor ini saja sudah menyumbang bagian besar dari speedup.
Yang tidak ikut lebih cepat: pemanggilan ekstensi C (NumPy, pandas), operasi I/O (jaringan, disk), dan operasi yang didominasi alokasi memori (membuat jutaan objek kecil). Kalau program Python kamu menghabiskan 95% waktunya di ekstensi C dan 5% di Python murni, JIT hanya mempercepat 5% itu — terukur, tapi tidak mengubah keadaan.
Pipeline Spesialisasi
JIT tidak bekerja sendirian. Ia adalah tahap akhir dari pipeline performa yang dimulai dari specializing interpreter Python 3.11 dan dilanjutkan dengan peningkatan bertahap di 3.12-3.14.
- Tier 0: Interpreter. Semua kode dimulai dari sini. Interpretasi bytecode standar dengan spesialisasi adaptif. Setelah sebuah fungsi dipanggil beberapa kali, instruksi yang sering dipakai diganti dengan versi yang dispesialisasi tipe.
- Tier 1: Bytecode hasil JIT. JIT copy-and-patch mengompilasi bytecode yang sudah dispesialisasi menjadi machine code. Ini menghilangkan overhead dispatch dan memungkinkan optimisasi dasar seperti constant folding dan dead code elimination di dalam template yang sudah dikompilasi.
- Tier 2 (masa depan): Optimisasi berbasis trace. Merekam jejak eksekusi melalui jalur kode yang panas lalu mengompilasi seluruh trace — melintasi batas fungsi — menjadi machine code yang teroptimasi. Ini sudah direncanakan tapi belum dirilis.
Pendekatan bertingkat ini mirip dengan yang dipakai YJIT Ruby dan runtime bahasa modern lainnya. Mulai dengan interpretasi yang cepat, pindah ke kompilasi yang cepat saat kode mulai panas, dan simpan optimisasi yang mahal untuk jalur terpanas. Wawasan dasarnya sama: sebagian besar kode tidak layak dioptimasi, jadi belanjakan anggaran kompilasi untuk kode yang paling sering dijalankan.
Dampak ke Memori dan Waktu Startup
JIT compiler memakai memori untuk kode yang sudah dikompilasi. Overhead memori copy-and-patch JIT tergolong wajar — kode hasil kompilasi lebih besar dari bytecode tapi lebih kecil dibanding yang dihasilkan JIT berbasis LLVM (karena tidak ada bloat dari optimisasi). Implementasi saat ini memakai sekitar 1,5-3x memori dari bytecode yang digantikannya, dan hanya mengompilasi fungsi yang dipanggil cukup sering sehingga memang layak.
Waktu startup jadi perhatian untuk skrip Python yang berumur pendek. JIT menambah overhead untuk memuat library template dan menyiapkan infrastruktur kompilasi. Untuk skrip yang berjalan kurang dari satu detik, overhead JIT bisa lebih besar dari speedup yang didapat. CPython mengatasinya dengan hanya mengompilasi fungsi setelah dipanggil sejumlah threshold tertentu — skrip berumur pendek tetap di interpreter dan tidak membayar 'pajak' JIT sama sekali.
Ini bisa dikonfigurasi. Flag -X jit mengatur perilaku JIT, dan variabel environment bisa menyetel threshold kompilasi. Untuk fungsi serverless dan CLI tool yang startup-nya penting, kamu bisa menaikkan threshold atau mematikan JIT sepenuhnya. Untuk server yang berjalan lama dan skrip pengolahan data yang performa steady-state-nya penting, pengaturan default sudah cukup bagus.
Apa Artinya untuk Ekosistem Python
JIT tidak mengubah posisi Python dalam hierarki performa — C, Rust, Go, dan Java tetap jauh lebih cepat untuk pekerjaan yang berat secara komputasi. Yang berubah adalah titik di mana developer Python perlu beralih ke alternatif tersebut.
Speedup 20-30% pada kode Python murni berarti beberapa beban kerja yang sebelumnya butuh ekstensi C atau penulisan ulang sekarang sudah cukup cepat dengan Python murni. Skrip pengolahan data yang tadinya 10 menit jadi 7 menit. Web server yang tadinya melayani 1000 request per detik jadi 1300. Angkanya memang bukan revolusioner, tapi di sinilah bedanya antara 'Python sudah cukup cepat' dan 'kita harus tulis ulang ini pakai Go'.
Yang lebih penting, infrastruktur JIT ini membuka fondasi untuk optimisasi di masa depan. Pendekatan copy-and-patch bisa diperluas dengan template yang lebih baik, spesialisasi yang lebih banyak, dan akhirnya kompilasi berbasis trace. Setiap versi Python bisa merilis template yang lebih baik tanpa mengubah arsitektur dasar JIT. Speedup 20-30% di 3.15 adalah lantai, bukan langit-langit.
Setelah tiga dekade sebagai salah satu bahasa paling populer sekaligus paling lambat di dunia, CPython akhirnya serius berinvestasi di performa. JIT ini memang tidak akan memuaskan kelompok 'Python terlalu lambat' — tidak ada yang bisa, karena untuk sebagian beban kerja Python memang benar-benar lambat, dengan atau tanpa JIT. Tapi untuk mayoritas kode Python, yang kecepatannya 'cukup oke tapi belum bagus', JIT mendekatkannya ke kategori 'benar-benar bagus'. Ini lebih besar dari kedengarannya.


