Pembenci Python selalu mengatakan bahwa salah satu alasan mereka tidak ingin menggunakan bahasa ini adalah karena Python lambat. Tetapi fakta bahwa suatu program tertentu, terlepas dari bahasa pemrograman yang digunakan, dapat dianggap cepat atau lambat, sangat tergantung pada pengembang yang menulisnya, pada pengetahuannya dan pada kemampuan untuk membuat kode yang dioptimalkan dan berkinerja tinggi.

Penulis artikel, yang kami terbitkan hari ini, menawarkan untuk membuktikan bahwa mereka yang memanggil Python lambat adalah salah. Dia ingin berbicara tentang cara meningkatkan kinerja program Python dan membuatnya sangat cepat.
Pengukuran Waktu dan Pembuatan Profil
Sebelum Anda mulai mengoptimalkan kode apa pun, Anda harus terlebih dahulu mengetahui bagian mana yang memperlambat seluruh program. Kadang-kadang hambatan program mungkin terlihat jelas, tetapi jika programmer tidak tahu di mana itu, ia dapat memanfaatkan beberapa peluang untuk mengidentifikasinya.
Di bawah ini adalah kode program, yang akan saya gunakan untuk keperluan demo. Ini diambil dari dokumentasi Python. Kode ini meningkatkan
e
menjadi kekuatan
x
:
Cara termudah untuk "profil" kode
Untuk memulai, pertimbangkan cara paling sederhana untuk membuat profil kode Anda. Jadi bisa dikatakan, "membuat profil untuk yang malas." Itu terdiri dalam menggunakan perintah
time
Unix:
~ $ time python3.8 slow_program.py real 0m11,058s user 0m11,050s sys 0m0,008s
Pembuatan profil semacam itu mungkin memberi programmer informasi yang berguna - jika ia perlu mengukur waktu pelaksanaan seluruh program. Tetapi biasanya ini tidak cukup.
Metode Pembuatan Profil Yang Paling Akurat
Di ujung lain dari spektrum metode kode profiling terletak alat
cProfile
, yang memberikan programmer, diakui, terlalu banyak informasi:
~ $ python3.8 -m cProfile -s time slow_program.py 1297 function calls (1272 primitive calls) in 11.081 seconds Ordered by: internal time ncalls tottime percall cumtime percall filename:lineno(function) 3 11.079 3.693 11.079 3.693 slow_program.py:4(exp) 1 0.000 0.000 0.002 0.002 {built-in method _imp.create_dynamic} 4/1 0.000 0.000 11.081 11.081 {built-in method builtins.exec} 6 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x9d12c0} 6 0.000 0.000 0.000 0.000 abc.py:132(__new__) 23 0.000 0.000 0.000 0.000 _weakrefset.py:36(__init__) 245 0.000 0.000 0.000 0.000 {built-in method builtins.getattr} 2 0.000 0.000 0.000 0.000 {built-in method marshal.loads} 10 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:1233(find_spec) 8/4 0.000 0.000 0.000 0.000 abc.py:196(__subclasscheck__) 15 0.000 0.000 0.000 0.000 {built-in method posix.stat} 6 0.000 0.000 0.000 0.000 {built-in method builtins.__build_class__} 1 0.000 0.000 0.000 0.000 __init__.py:357(namedtuple) 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:57(_path_join) 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:59(<listcomp>) 1 0.000 0.000 11.081 11.081 slow_program.py:1(<module>)
Di sini kita menjalankan skrip yang diselidiki menggunakan modul
cProfile
dan menggunakan argumen
time
. Akibatnya, jalur output diurutkan berdasarkan waktu internal (
cumtime
). Ini memberi kita banyak informasi. Padahal, yang ditunjukkan di atas hanya sekitar 10% dari output
cProfile
.
Setelah menganalisis data ini, kita dapat melihat bahwa fungsi
exp
adalah alasan untuk operasi yang lambat dari program (itu mengejutkan!). Setelah itu, kita dapat melakukan pembuatan kode menggunakan alat yang lebih akurat.
Studi tentang indikator kinerja sementara dari fungsi tertentu
Sekarang kita tahu tentang tempat program di mana kita perlu mengarahkan perhatian kita. Oleh karena itu, kami dapat memutuskan untuk mempelajari fungsi lambat tanpa membuat profil kode program lainnya. Untuk melakukan ini, Anda dapat menggunakan dekorator sederhana:
def timeit_wrapper(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter()
Dekorator ini dapat diterapkan pada fungsi yang akan dieksplorasi:
@timeit_wrapper def exp(x): ... print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time')) exp(Decimal(150)) exp(Decimal(400)) exp(Decimal(3000))
Sekarang setelah memulai program kami akan menerima informasi berikut:
~ $ python3.8 slow_program.py module function time __main__ .exp : 0.003267502994276583 __main__ .exp : 0.038535295985639095 __main__ .exp : 11.728486061969306
Di sini perlu diperhatikan kapan tepatnya kita berencana mengukur. Paket yang sesuai memberi kami indikator seperti
time.perf_counter
dan
time.perf_counter
. Perbedaan di antara mereka adalah
perf_counter
mengembalikan nilai absolut, yang mencakup waktu selama proses program Python tidak berjalan. Ini berarti bahwa indikator ini dapat dipengaruhi oleh beban pada komputer yang dibuat oleh program lain.
process_time
hanya mengembalikan waktu pengguna. Itu tidak termasuk waktu sistem. Ini hanya memberi kami informasi tentang waktu pelaksanaan proses kami.
Akselerasi kode
Dan sekarang untuk bagian yang menyenangkan. Mari kita bekerja mempercepat program. Saya (sebagian besar) tidak akan menunjukkan semua jenis peretasan, trik, dan potongan kode misterius di sini yang secara ajaib menyelesaikan masalah kinerja. Saya pada dasarnya ingin berbicara tentang ide dan strategi umum yang, jika digunakan, dapat berdampak besar pada kinerja. Dalam beberapa kasus, kita berbicara tentang peningkatan 30% dalam kecepatan eksekusi kode.
▍Gunakan tipe data bawaan
Menggunakan tipe data bawaan adalah pendekatan yang sangat jelas untuk mempercepat kode. Tipe data bawaan sangat cepat, terutama jika Anda membandingkannya dengan tipe khusus seperti pohon atau daftar tertaut. Intinya di sini adalah terutama bahwa mekanisme built-in dari bahasa diimplementasikan menggunakan C. Jika Anda menggambarkan sesuatu menggunakan Python, Anda tidak dapat mencapai tingkat kinerja yang sama.
▍Menerapkan caching (memoisasi) dengan lru_cache
Caching adalah pendekatan populer untuk meningkatkan kinerja kode. Saya sudah
menulis tentang dia, tetapi saya pikir ada baiknya menceritakan tentang dia di sini:
import functools import time
Fungsi di atas mensimulasikan perhitungan kompleks menggunakan
time.sleep
. Ketika dipanggil untuk pertama kalinya dengan parameter
1
, ia menunggu 2 detik dan mengembalikan hasilnya hanya setelah itu. Ketika dia dipanggil lagi dengan parameter yang sama, ternyata hasil karyanya sudah di-cache. Tubuh fungsi dalam situasi ini tidak dieksekusi, dan hasilnya dikembalikan segera.
Di sini Anda dapat menemukan contoh-contoh caching yang lebih dekat dengan kenyataan.
▍Gunakan variabel lokal
Menerapkan variabel lokal, kami memperhitungkan kecepatan pencarian variabel di setiap cakupan. Saya berbicara secara khusus tentang "setiap bidang visibilitas", karena di sini yang saya pikirkan bukan hanya perbandingan kecepatan kerja dengan variabel lokal dan global. Bahkan, perbedaan dalam bekerja dengan variabel bahkan diamati, katakanlah, antara variabel lokal dalam suatu fungsi (kecepatan tertinggi), atribut tingkat kelas (misalnya,
self.name
, ini sudah lebih lambat), dan entitas impor global seperti
time.time
(paling lambat dari ketiga mekanisme ini).
Anda dapat meningkatkan kinerja menggunakan pendekatan berikut untuk menetapkan nilai yang mungkin tampak benar-benar tidak perlu dan tidak berguna untuk orang yang tidak mendapat informasi:
▍ Bungkus kode dalam fungsi
Saran ini mungkin tampak bertentangan dengan akal sehat, karena ketika suatu fungsi dipanggil, beberapa data didorong ke stack dan sistem berada di bawah beban tambahan yang memproses operasi kembali dari fungsi. Namun, rekomendasi ini terkait dengan yang sebelumnya. Jika Anda hanya meletakkan semua kode Anda dalam satu file tanpa menuliskannya sebagai fungsi, itu akan berjalan jauh lebih lambat karena penggunaan variabel global. Ini berarti bahwa kode dapat dipercepat hanya dengan membungkusnya di fungsi
main()
dan memanggilnya sekali:
def main(): ...
▍Jangan mengakses atribut
Mekanisme lain yang dapat memperlambat program adalah operator dot (
.
), Yang digunakan untuk mengakses atribut objek. Pernyataan ini meminta
__getattribute__
kamus menggunakan
__getattribute__
, yang
__getattribute__
tekanan ekstra pada sistem. Bagaimana cara membatasi dampak kinerja fitur Python ini?
▍ Waspadai string
Operasi string dapat sangat memperlambat program jika dijalankan dalam loop. Secara khusus, kita berbicara tentang memformat string menggunakan
%s
dan
.format()
. Apakah mungkin untuk menggantinya dengan sesuatu? Jika Anda melihat
tweet terbaru
dari Raymond Hettinger, Anda dapat melihat bahwa satu-satunya mekanisme yang perlu digunakan dalam situasi seperti itu adalah f-line. Ini adalah metode pemformatan string yang paling mudah dibaca, ringkas, dan tercepat. Di sini, sesuai dengan tweet itu, adalah daftar metode yang dapat digunakan untuk bekerja dengan string - dari yang tercepat hingga yang paling lambat:
f'{s} {t}'
▍Ketahui bahwa generator juga dapat bekerja dengan cepat
Generator bukanlah mekanisme yang sifatnya cepat. Faktanya adalah bahwa mereka diciptakan untuk melakukan perhitungan "malas", yang tidak menghemat waktu, tetapi memori. Namun, menghemat memori dapat menyebabkan program berjalan lebih cepat. Bagaimana ini mungkin? Faktanya adalah bahwa ketika memproses kumpulan data besar tanpa menggunakan generator (iterators), data dapat menyebabkan limpahan cache L1 prosesor, yang secara signifikan akan memperlambat proses pencarian nilai dalam memori.
Ketika datang ke kinerja, sangat penting untuk berusaha untuk memastikan bahwa prosesor dapat dengan cepat mengakses data yang diprosesnya, sehingga mereka sedekat mungkin dengannya. Dan ini berarti bahwa data tersebut harus ditempatkan di cache prosesor. Masalah ini dibahas dalam presentasi
ini oleh Raymond Hettinger.
Ringkasan
Aturan optimasi pertama adalah optimasi itu tidak perlu. Tetapi jika Anda tidak dapat melakukannya tanpa optimasi, maka saya berharap tips yang saya bagikan akan membantu Anda dalam hal ini.
Pembaca yang budiman! Bagaimana Anda mendekati mengoptimalkan kinerja kode Python Anda?
