Bytecode dan dis

Bytecode yang benar-benar dieksekusi CPython dan pemeriksaannya dengan dis.

Daftar isi

CPython tidak mengeksekusi kode sumber Python secara langsung. Sebelum sebuah program berjalan, kode sumber terlebih dahulu dikompilasi menjadi bytecode, yaitu rangkaian instruksi untuk mesin virtual yang menjadi inti CPython. Topik ini membahas proses kompilasi tersebut, objek kode yang dihasilkannya, serta pemeriksaan bytecode menggunakan modul dis. Setelah membacanya, instruksi yang sesungguhnya dijalankan interpreter dapat dilihat apa adanya, dan klaim tentang performa dapat dihubungkan dengan instruksi yang benar-benar berjalan.

Dari kode sumber ke bytecode#

Kompilasi terjadi saat sebuah modul dimuat, bukan sekali untuk seluruh sesi. CPython membaca kode sumber, mengurainya menjadi pohon sintaks, lalu mengompilasinya menjadi bytecode untuk mesin virtual berbasis tumpukan (stack-based), yaitu mesin yang bekerja dengan menumpuk dan mengambil nilai pada sebuah tumpukan. Hasil kompilasi di-cache sebagai berkas .pyc di dalam folder __pycache__, sehingga pemuatan berikutnya tidak perlu mengompilasi ulang. Setiap berkas .pyc mencantumkan penanda versi, sehingga bytecode buatan Python 3.12 tidak dipakai oleh Python 3.13.

Hasil kompilasi berwujud objek kode (code object), yaitu objek yang menyimpan bytecode beserta nama-nama lokal, nama-nama global yang dirujuk, dan konstanta yang dipakai. Fungsi yang dibuat melalui def membawa objek kode ini pada atribut __code__, sedangkan fungsi compile() dapat membangunnya dari string kode sumber.

python Contoh: atribut objek kode
def hitung(n):
    total = 0
    for i in range(n):
        total += i
    return total

kode = hitung.__code__
print(kode.co_name)
print(kode.co_names)
print(kode.co_varnames)
print(kode.co_consts)
text Keluaran
hitung
('range',)
('n', 'total', 'i')
(None, 0)

Atribut co_names hanya memuat range karena itulah satu-satunya nama global yang dirujuk, sedangkan co_varnames memuat n, total, dan i sebagai variabel lokal. Atribut co_consts menyimpan konstanta yang muncul di dalam fungsi, termasuk None yang menjadi nilai kembalian apabila fungsi berakhir tanpa return yang membawa nilai. Diagram berikut merangkum alurnya dari kode sumber sampai eksekusi.

Kode sumber.pyKompilasisaat dimuatBytecode.pyc di __pycache__CPython VMmesin tumpukan
Alur dari kode sumber menjadi bytecode, lalu dieksekusi mesin virtual CPython

Membaca keluaran dis#

Modul dis menampilkan isi objek kode sebagai daftar instruksi yang dapat dibaca. Setiap baris keluarannya memuat nomor baris kode sumber, offset byte, nama opcode (kode operasi yang dieksekusi), serta argumen beserta penjelasannya. Fungsi dis.dis() menerima fungsi, metode, objek kode, bahkan string kode sumber.

python Contoh: instruksi fungsi kecil
import dis

def total(qty):
    return qty * 2 + 1

dis.dis(total)
text Keluaran
  3           0 RESUME                   0

  4           2 LOAD_FAST                0 (qty)
              4 LOAD_CONST               1 (2)
              6 BINARY_OP                5 (*)
             10 LOAD_CONST               2 (1)
             12 BINARY_OP                0 (+)
             16 RETURN_VALUE

Keluaran di atas memperlihatkan cara kerja mesin tumpukan secara utuh. Instruksi LOAD_FAST menaruh nilai variabel lokal qty ke tumpukan, LOAD_CONST menaruh angka 2, lalu BINARY_OP mengambil dua nilai teratas dari tumpukan, mengalikannya, dan menaruh hasilnya kembali. Langkah yang sama berulang untuk penjumlahan dengan 1, dan RETURN_VALUE mengembalikan nilai teratas tumpukan kepada pemanggil.

python Contoh: instruksi perulangan
import dis

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

dis.dis(hitung)
text Keluaran
  3           0 RESUME                   0

  4           2 LOAD_CONST               1 (0)
              4 STORE_FAST               1 (total)

  5           6 LOAD_GLOBAL              1 (NULL + range)
             16 LOAD_FAST                0 (n)
             18 CALL                     1
             26 GET_ITER
        >>   28 FOR_ITER                 7 (to 46)
             32 STORE_FAST               2 (i)

  6          34 LOAD_FAST                1 (total)
             36 LOAD_FAST                2 (i)
             38 BINARY_OP               13 (+=)
             42 STORE_FAST               1 (total)
             44 JUMP_BACKWARD            9 (to 28)

  5     >>   46 END_FOR

  7          48 LOAD_FAST                1 (total)
             50 RETURN_VALUE

Perulangan for berwujud pasangan FOR_ITER dan JUMP_BACKWARD, yaitu instruksi mengambil elemen berikutnya dan instruksi melompat kembali ke awal badan perulangan. Nama range diambil lewat LOAD_GLOBAL karena berada di lingkup global, dan pencarian semacam ini lebih mahal daripada LOAD_FAST pada variabel lokal. Konsekuensi praktisnya, nama global yang dipanggil berulang dalam perulangan panjang dapat diikat ke variabel lokal terlebih dahulu agar aksesnya dipercepat oleh interpreter.

Pelipatan konstanta saat kompilasi#

Kompilator CPython menyertakan pengoptimal sederhana yang mengevaluasi ekspresi berkonstanta saat kompilasi, teknik yang dikenal sebagai constant folding. Ekspresi seperti 24 * 60 karenanya tidak dihitung saat program berjalan, melainkan sudah tergantikan oleh hasilnya di dalam bytecode. Perbandingan dua fungsi berikut menunjukkan kasus yang terlipat dan yang tidak.

python Contoh: konstanta yang dilipat dan yang tidak
import dis

def menit_per_hari():
    return 24 * 60

def sapa(nama):
    pesan = "Halo, " + nama
    return pesan

dis.dis(menit_per_hari)
print("---")
dis.dis(sapa)
text Keluaran
  3           0 RESUME                   0

  4           2 RETURN_CONST             1 (1440)
---
  6           0 RESUME                   0

  7           2 LOAD_CONST               1 ('Halo, ')
              4 LOAD_FAST                0 (nama)
              6 BINARY_OP                0 (+)
             10 STORE_FAST               1 (pesan)

  8          12 LOAD_FAST                1 (pesan)
             14 RETURN_VALUE

Fungsi pertama hanya memuat RETURN_CONST 1440, yaitu hasil 24 * 60 yang sudah selesai dihitung saat kompilasi, tanpa satu pun BINARY_OP. Fungsi kedua tetap menghitung "Halo, " + nama saat program berjalan karena isi variabel nama baru diketahui saat eksekusi, sehingga tidak dapat dilipat. Dampak pelipatan terhadap performa kecil, tetapi pola ini menegaskan bahwa sebagian kerja sudah selesai sebelum eksekusi dimulai, dan menulis 24 * 60 secara eksplisit justru lebih jelas daripada angka 1440 yang misterius.

Karena pelipatan bekerja hanya pada ekspresi yang seluruh operannya berupa konstanta, bentuk yang dilipat terbatas pada perhitungan aritmetika dan penggabungan literal string. Ekspresi yang melibatkan nama, hasil fungsi, atau nilai masukan tetap dikompilasi menjadi instruksi yang dihitung setiap kali dijalankan. Pembacaan dis dengan demikian menjadi cara cepat memastikan komputasi tertentu sudah selesai di waktu kompilasi atau masih menunggu waktu eksekusi.

Kesalahan umum#

Keluaran dis sering dianggap kontrak yang stabil, padahal bentuk bytecode berubah antar versi. Offset, nama opcode, bahkan jumlah instruksinya dapat berbeda untuk fungsi yang sama, karena tiap versi CPython menyempurnakan kumpulan instruksinya. Karena itu pengujian otomatis tidak boleh membandingkan teks bytecode, dan penilaian performa tetap diserahkan kepada pengukuran sebagaimana dibahas pada topik profiling dan benchmarking.

Kesalahpahaman kedua adalah menganggap bytecode setara kode mesin. Bytecode tetap ditafsirkan oleh mesin virtual CPython, bukan diterjemahkan menjadi instruksi prosesor secara penuh, sehingga kecepatan program tetap ditentukan oleh kualitas interpreter. Berkas .pyc pun kerap disalahpahami, yaitu dianggap rusak apabila tidak terbaca, padahal berkas yang tidak cocok dengan versi interpreter hanya diabaikan dan modulnya dikompilasi ulang tanpa error.

Kesalahan ketiga adalah menilai kecepatan hanya dari jumlah instruksinya. Instruksi yang lebih sedikit biasanya lebih cepat, tetapi biaya tiap opcode tidak seragam, dan versi baru CPython dapat mengganti instruksi panjang menjadi serangkaian instruksi khusus yang lebih cepat meskipun lebih banyak. Jumlah instruksinya dengan demikian berfungsi sebagai petunjuk arah, sedangkan putusannya tetap diserahkan kepada pengukuran.

Ringkasan#

  • CPython mengompilasi kode sumber menjadi bytecode saat modul dimuat, dan hasilnya di-cache sebagai berkas .pyc di __pycache__ yang tidak berlaku lintas versi.
  • Bytecode tersimpan dalam objek kode yang dapat diperiksa melalui atribut __code__ dan didekompilasi dengan dis.dis().
  • dis.dis() menampilkan nomor baris, offset byte, nama opcode, dan argumennya, sehingga kerja mesin tumpukan dapat dibaca baris demi baris.
  • Ekspresi berkonstanta dilipat saat kompilasi, sedangkan bentuk bytecode berubah antar versi sehingga tidak layak dijadikan kontrak pengujian.

Referensi#