def main():
for i in xrange(10**8):
pass
main()
Sepotong kode dalam Python ini berjalan di (Catatan: Pengaturan waktu dilakukan dengan fungsi waktu dalam BASH di Linux.)
real 0m1.841s
user 0m1.828s
sys 0m0.012s
Namun, jika for loop tidak ditempatkan dalam suatu fungsi,
for i in xrange(10**8):
pass
kemudian berjalan untuk waktu yang lebih lama:
real 0m4.543s
user 0m4.524s
sys 0m0.012s
Kenapa ini?
python
performance
profiling
benchmarking
cpython
thedoctar
sumber
sumber
Jawaban:
Anda mungkin bertanya mengapa lebih cepat menyimpan variabel lokal daripada global. Ini adalah detail implementasi CPython.
Ingat bahwa CPython dikompilasi ke bytecode, yang dijalankan oleh interpreter. Ketika suatu fungsi dikompilasi, variabel lokal disimpan dalam array ukuran tetap ( bukan a
dict
) dan nama variabel ditugaskan ke indeks. Ini dimungkinkan karena Anda tidak dapat menambahkan variabel lokal secara dinamis ke suatu fungsi. Kemudian mengambil variabel lokal secara harfiah adalah pencarian pointer ke dalam daftar dan peningkatan jumlah referensiPyObject
yang sepele.Bandingkan ini dengan pencarian global (
LOAD_GLOBAL
), yang merupakandict
pencarian sebenarnya yang melibatkan hash dan sebagainya. Kebetulan, inilah mengapa Anda perlu menentukanglobal i
apakah Anda ingin menjadi global: jika Anda pernah menetapkan variabel di dalam lingkup, kompiler akan mengeluarkanSTORE_FAST
s untuk aksesnya kecuali jika Anda tidak melakukannya.Omong-omong, pencarian global masih cukup optimal. Pencarian atribut
foo.bar
adalah yang sangat lambat!Berikut adalah ilustrasi kecil tentang efisiensi variabel lokal.
sumber
def foo_func: x = 5
,x
bersifat lokal untuk suatu fungsi. Mengaksesx
adalah lokal.foo = SomeClass()
,foo.bar
adalah akses atribut.val = 5
global adalah global. Adapun atribut local> global> speed sesuai dengan apa yang saya baca di sini. Jadi mengaksesx
difoo_func
adalah tercepat, diikuti olehval
, diikuti olehfoo.bar
.foo.attr
bukan pencarian lokal karena dalam konteks obrolan ini, kita berbicara tentang pencarian lokal menjadi pencarian variabel yang termasuk dalam fungsi.globals()
fungsinya. Jika Anda ingin info lebih dari itu, Anda mungkin harus mulai mencari kode sumber untuk Python. Dan CPython hanyalah nama untuk implementasi Python yang biasa - jadi Anda mungkin sudah menggunakannya!Di dalam suatu fungsi, bytecode adalah:
Di tingkat atas, bytecode adalah:
Perbedaannya adalah bahwa
STORE_FAST
lebih cepat (!) DaripadaSTORE_NAME
. Ini karena dalam suatu fungsi,i
adalah lokal tetapi pada tingkat atas itu adalah global.Untuk memeriksa bytecode, gunakan
dis
modul . Saya dapat membongkar fungsi secara langsung, tetapi untuk membongkar kode tingkat atas saya harus menggunakancompile
builtin .sumber
global i
ke dalammain
fungsi membuat waktu berjalan setara.locals()
, atauinspect.getframe()
dll.). Mencari elemen array dengan integer konstan jauh lebih cepat daripada mencari dict.Selain waktu penyimpanan variabel lokal / global, prediksi opcode membuat fungsi lebih cepat.
Seperti jawaban lain menjelaskan, fungsi menggunakan
STORE_FAST
opcode di loop. Inilah bytecode untuk loop fungsi:Biasanya ketika sebuah program dijalankan, Python mengeksekusi setiap opcode satu demi satu, melacak stack dan melakukan preforming cek lainnya pada frame stack setelah setiap opcode dieksekusi. Prediksi opcode berarti bahwa dalam kasus-kasus tertentu Python dapat melompat langsung ke opcode berikutnya, sehingga menghindari beberapa overhead ini.
Dalam hal ini, setiap kali Python melihat
FOR_ITER
(bagian atas loop), itu akan "memprediksi" ituSTORE_FAST
adalah opcode berikutnya yang harus dijalankan. Python kemudian mengintip opcode berikutnya dan, jika prediksi itu benar, ia langsung melompatSTORE_FAST
. Ini memiliki efek memeras dua opcode menjadi satu opcode tunggal.Di sisi lain,
STORE_NAME
opcode digunakan dalam loop di tingkat global. Python tidak * tidak * membuat prediksi serupa ketika melihat opcode ini. Sebaliknya, itu harus kembali ke atas evaluasi-loop yang memiliki implikasi yang jelas untuk kecepatan di mana loop dieksekusi.Untuk memberikan detail teknis lebih lanjut tentang optimasi ini, berikut adalah kutipan dari
ceval.c
file ("mesin" mesin virtual Python):Kita dapat melihat dalam kode sumber untuk
FOR_ITER
opcode di mana prediksiSTORE_FAST
dibuat:The
PREDICT
Fungsi memperluas untukif (*next_instr == op) goto PRED_##op
yaitu kita hanya melompat ke awal dari opcode diprediksi. Dalam hal ini, kita lompat ke sini:Variabel lokal sekarang diatur dan opcode berikutnya siap untuk dieksekusi. Python terus melalui iterable hingga mencapai akhir, membuat prediksi sukses setiap saat.
The halaman wiki Python memiliki informasi lebih lanjut tentang bagaimana mesin virtual CPython ini bekerja.
sumber
HAS_ARG
pengujian tidak pernah terjadi (kecuali ketika pelacakan tingkat rendah diaktifkan pada saat kompilasi dan runtime, yang tidak dilakukan oleh bangunan normal), hanya menyisakan satu lompatan yang tidak dapat diprediksi.PREDICT
makro sepenuhnya dinonaktifkan; sebaliknya kebanyakan kasus berakhir denganDISPATCH
cabang yang secara langsung. Tetapi pada CPU prediksi cabang, efeknya mirip denganPREDICT
, karena percabangan (dan prediksi) adalah per opcode, meningkatkan kemungkinan prediksi cabang yang sukses.