Baru-baru ini, orang dapat mengamati semakin populernya bahasa pemrograman Python. Ini digunakan dalam DevOps, dalam analisis data, dalam pengembangan web, di bidang keamanan dan di bidang lainnya. Tapi inilah kecepatannya ... Tidak ada yang membanggakan dari bahasa ini di sini. Penulis materi, terjemahan yang kami terbitkan hari ini, memutuskan untuk mencari tahu alasan lambatnya Python dan menemukan cara untuk mempercepatnya.

Ketentuan Umum
Bagaimana Java, dalam hal kinerja, berhubungan dengan C atau C ++? Bagaimana cara membandingkan C # dan Python? Jawaban atas pertanyaan-pertanyaan ini sangat bergantung pada jenis aplikasi yang dianalisis oleh peneliti. Tidak ada patokan yang sempurna, tetapi mempelajari kinerja program yang ditulis dalam berbagai bahasa, The Computer Language Benchmarks Game bisa menjadi
titik awal yang baik.
Saya telah merujuk ke The Computer Benchmarks Game selama lebih dari sepuluh tahun. Python, dibandingkan dengan bahasa lain, seperti Java, C #, Go, JavaScript, C ++, adalah salah satu
yang paling lambat . Ini termasuk bahasa yang menggunakan kompilasi
JIT (C #, Java), dan kompilasi
AOT (C #, C ++), serta bahasa yang ditafsirkan seperti JavaScript.
Di sini saya ingin mencatat bahwa ketika saya mengatakan "Python", maksud saya adalah implementasi referensi dari interpreter Python - CPython. Dalam materi ini, kami akan menyentuh implementasi lainnya. Sebenarnya, di sini saya ingin menemukan jawaban untuk pertanyaan mengapa Python membutuhkan waktu 2-10 kali lebih banyak daripada bahasa lain untuk menyelesaikan masalah yang sebanding, dan apakah itu bisa dilakukan lebih cepat.
Berikut adalah beberapa teori dasar yang mencoba menjelaskan mengapa Python lambat:
- Alasannya adalah GIL (Global Interpreter Lock, Global Interpreter Lock).
- Alasannya adalah bahwa Python adalah bahasa yang ditafsirkan dan bukan dikompilasi.
- Alasannya adalah pengetikan dinamis.
Kami akan menganalisis ide-ide ini dan mencoba menemukan jawaban untuk pertanyaan tentang apa yang memiliki efek terbesar pada kinerja aplikasi Python.
Gil
Komputer modern memiliki prosesor multi-core, dan sistem multiprosesor kadang-kadang ditemukan. Untuk menggunakan semua kekuatan komputasi ini, sistem operasi menggunakan struktur tingkat rendah yang disebut utas, sementara proses (misalnya, proses peramban Chrome) dapat meluncurkan banyak utas dan menggunakannya sesuai dengannya. Akibatnya, misalnya, jika suatu proses sangat membutuhkan sumber daya prosesor, pelaksanaannya dapat dibagi antara beberapa core, yang memungkinkan sebagian besar aplikasi untuk menyelesaikan tugas yang mereka hadapi lebih cepat.
Sebagai contoh, browser Chrome saya, pada saat saya menulis ini, memiliki 44 utas terbuka. Harus diingat bahwa struktur dan API sistem untuk bekerja dengan stream berbeda dalam sistem operasi berbasis Posix (Mac OS, Linux) dan dalam keluarga sistem operasi Windows. Sistem operasi juga merencanakan utas.
Jika Anda belum pernah bertemu dengan pemrograman multi-thread sebelumnya, sekarang Anda harus berkenalan dengan apa yang disebut kunci (kunci). Arti dari penguncian adalah bahwa pengunciannya memungkinkan Anda untuk memastikan perilaku sistem tersebut ketika, dalam lingkungan multi-utas, misalnya, saat mengubah variabel tertentu dalam memori, beberapa utas tidak bisa mendapatkan akses ke area memori yang sama (untuk membaca atau mengubah).
Ketika juru bahasa CPython menciptakan variabel, itu mengalokasikan memori dan kemudian menghitung jumlah referensi yang ada untuk variabel-variabel ini. Konsep ini dikenal sebagai penghitungan referensi. Jika jumlah tautan sama dengan nol, maka bagian memori yang sesuai dibebaskan. Itulah sebabnya, misalnya, pembuatan variabel "sementara", katakanlah, dalam lingkup loop, tidak mengarah pada peningkatan berlebihan dalam jumlah memori yang dikonsumsi oleh aplikasi.
Bagian yang paling menarik dimulai ketika beberapa utas berbagi variabel yang sama, dan masalah utama di sini adalah bagaimana tepatnya CPython melakukan penghitungan referensi. Di sinilah aksi “global interpreter lock” muncul, yang dengan hati-hati mengontrol eksekusi utas.
Seorang juru bahasa hanya dapat melakukan satu operasi pada satu waktu, terlepas dari berapa banyak utas dalam program.
▍ Bagaimana GIL mempengaruhi kinerja aplikasi Python?
Jika kami memiliki aplikasi single-threaded yang berjalan dalam proses interpreter Python yang sama, maka GIL tidak mempengaruhi kinerja dengan cara apa pun. Misalnya, jika menyingkirkan GIL, kami tidak akan melihat adanya perbedaan dalam kinerja.
Jika, dalam kerangka satu proses interpreter Python, perlu untuk mengimplementasikan pemrosesan data paralel menggunakan mekanisme multithreading, dan aliran yang digunakan akan secara intensif menggunakan subsistem I / O (misalnya, jika mereka bekerja dengan jaringan atau dengan disk), maka akan mungkin untuk mengamati konsekuensi dari bagaimana GIL mengelola utas. Inilah yang terlihat dalam kasus menggunakan dua utas, proses pemuatan intensif.
Visualisasi GIL (diambil dari sini )Jika Anda memiliki aplikasi web (misalnya, berdasarkan pada kerangka Django) dan Anda menggunakan WSGI, maka setiap permintaan untuk aplikasi web akan dilayani oleh proses interpreter Python yang terpisah, yaitu, kami hanya memiliki 1 kunci permintaan. Karena interpreter Python mulai lambat, dalam beberapa implementasi WSGI ada yang disebut "mode daemon", ketika menggunakan
proses interpreter yang dipertahankan dalam kondisi kerja, yang memungkinkan sistem untuk melayani permintaan lebih cepat.
▍ Bagaimana perilaku juru bahasa Python lainnya?
PyPy memiliki GIL, biasanya lebih dari 3 kali lebih cepat dari CPython.
Tidak ada GIL di Jython, karena utas Python di Jython direpresentasikan sebagai utas Java. Utas semacam itu menggunakan kemampuan manajemen memori JVM.
▍ Bagaimana kontrol aliran diatur dalam JavaScript?
Jika kita berbicara tentang JavaScript, maka, pertama-tama, harus dicatat bahwa semua mesin JS menggunakan algoritma pengumpulan sampah
mark-and-sweep . Seperti yang telah disebutkan, alasan utama untuk menggunakan GIL adalah algoritma manajemen memori yang digunakan dalam CPython.
JavaScript tidak memiliki GIL, namun, JS adalah bahasa single-threaded, oleh karena itu, tidak perlu mekanisme seperti itu. Alih-alih eksekusi kode paralel, JavaScript menggunakan teknik pemrograman asinkron berdasarkan loop peristiwa, janji, dan panggilan balik. Python memiliki sesuatu yang serupa yang disediakan oleh modul
asyncio
.
Python - bahasa yang ditafsirkan
Saya sering mendengar bahwa buruknya kinerja Python disebabkan oleh fakta bahwa itu adalah bahasa yang ditafsirkan. Pernyataan seperti itu didasarkan pada penyederhanaan besar tentang bagaimana sebenarnya CPython bekerja. Jika, di terminal, Anda memasukkan perintah seperti
python myscript.py
, maka CPython akan memulai serangkaian tindakan yang panjang, yang terdiri dari pembacaan, analisis leksikal, penguraian, kompilasi, menafsirkan, dan mengeksekusi kode skrip. Jika Anda tertarik dengan detailnya, lihat materi
ini .
Bagi kami, ketika mempertimbangkan proses ini, sangat penting bahwa di sini, pada tahap kompilasi, file
.pyc
dibuat, dan urutan bytecodes ditulis ke file dalam direktori
__pycache__/
, yang digunakan dalam Python 3 dan Python 2.
Ini tidak hanya berlaku untuk skrip yang kami tulis, tetapi juga untuk kode yang diimpor, termasuk modul pihak ketiga.
Akibatnya, sebagian besar waktu (kecuali jika Anda menulis kode yang hanya berjalan sekali), Python akan menjalankan bytecode yang sudah selesai. Membandingkan ini dengan apa yang terjadi di Java dan C #, ternyata kode Java dikompilasi ke dalam "Bahasa Antara", dan mesin virtual Java membaca bytecode dan melakukan kompilasi JIT-nya ke dalam kode mesin. "Bahasa perantara". NET CIL (yang sama dengan .NET Common-Language-Runtime, CLR) menggunakan kompilasi JIT untuk menavigasi ke kode mesin.
Sebagai hasilnya, baik di Jawa maupun di C # beberapa “bahasa perantara” digunakan dan mekanisme yang serupa ada. Jadi, mengapa Python menunjukkan tolok ukur yang jauh lebih buruk daripada Java dan C # jika semua bahasa ini menggunakan mesin virtual dan semacam bytecode? Pertama-tama, karena fakta bahwa kompilasi JIT digunakan dalam .NET dan Java.
Kompilasi JIT (kompilasi Just In Time, kompilasi on-the-fly atau just-in-time) membutuhkan bahasa perantara untuk memungkinkan pemisahan kode menjadi fragmen (bingkai). Sistem kompilasi AOT (kompilasi Ahead Of Time, kompilasi sebelum eksekusi) dirancang sedemikian rupa untuk memastikan fungsionalitas penuh kode sebelum interaksi kode ini dengan sistem dimulai.
Dengan sendirinya, menggunakan JIT tidak mempercepat eksekusi kode, karena beberapa fragmen kode byte dijalankan, seperti pada Python. Namun, JIT memungkinkan Anda untuk melakukan optimasi kode selama eksekusi. Pengoptimal JIT yang baik dapat mengidentifikasi bagian aplikasi yang paling banyak dimuat (bagian aplikasi ini disebut "hot spot") dan mengoptimalkan fragmen kode yang sesuai, menggantikannya dengan opsi yang dioptimalkan dan lebih produktif daripada yang digunakan sebelumnya.
Ini berarti bahwa ketika aplikasi tertentu melakukan tindakan tertentu berulang-ulang, optimasi tersebut dapat secara signifikan mempercepat pelaksanaan tindakan tersebut. Perlu diingat juga bahwa Java dan C # adalah bahasa yang diketik dengan kuat, sehingga pengoptimal dapat membuat lebih banyak asumsi tentang kode yang dapat membantu meningkatkan kinerja program.
Ada kompiler JIT di PyPy, dan, seperti yang telah disebutkan, implementasi juru bahasa Python ini jauh lebih cepat daripada CPython. Informasi tentang membandingkan berbagai penafsir Python dapat ditemukan dalam artikel
ini .
▍ Mengapa CPython tidak menggunakan kompiler JIT?
Kompiler JIT juga memiliki kelemahan. Salah satunya adalah waktu peluncuran. CPython sudah mulai relatif lambat, dan PyPy 2-3 kali lebih lambat dari CPython. Waktu jangka panjang JVM juga merupakan fakta yang diketahui. CLR .NET menghindari masalah ini dengan memulai selama boot sistem, tetapi harus dicatat bahwa CLR dan sistem operasi yang menjalankan CLR dikembangkan oleh perusahaan yang sama.
Jika Anda memiliki satu proses Python yang telah berjalan untuk waktu yang lama, sementara dalam proses semacam itu ada kode yang dapat dioptimalkan, karena mengandung bagian yang banyak digunakan, maka Anda harus serius melihat seorang juru bahasa yang memiliki kompiler JIT.
Namun, CPython adalah implementasi dari juru bahasa Python tujuan umum. Oleh karena itu, jika Anda mengembangkan, menggunakan Python, aplikasi baris perintah, maka kebutuhan untuk menunggu lama untuk kompiler JIT untuk memulai setiap kali aplikasi ini diluncurkan akan sangat memperlambat pekerjaan.
CPython berusaha memberikan dukungan untuk sebanyak mungkin kasus penggunaan Python. Sebagai contoh, ada kemungkinan menghubungkan kompiler JIT ke Python, bagaimanapun,
proyek yang mengimplementasikan ide ini tidak berkembang sangat aktif.
Sebagai hasilnya, kita dapat mengatakan bahwa jika Anda menggunakan Python untuk menulis sebuah program yang kinerjanya dapat meningkat ketika menggunakan kompiler JIT, gunakan juru bahasa PyPy.
Python adalah bahasa yang diketik secara dinamis
Dalam bahasa yang diketik secara statis, saat mendeklarasikan variabel, Anda harus menentukan jenisnya. Di antara bahasa-bahasa ini dapat dicatat C, C ++, Java, C #, Go.
Dalam bahasa yang diketik secara dinamis, konsep tipe data memiliki arti yang sama, tetapi tipe variabelnya dinamis.
a = 1 a = "foo"
Dalam contoh paling sederhana ini, Python pertama-tama menciptakan variabel pertama, lalu yang kedua dengan nama yang sama dari tipe
str
, dan membebaskan memori yang dialokasikan untuk variabel pertama
a
.
Tampaknya menulis dalam bahasa dengan pengetikan dinamis lebih mudah dan lebih sederhana daripada dalam bahasa dengan pengetikan statis, namun, bahasa tersebut tidak dibuat atas kemauan seseorang. Selama pengembangannya, fitur sistem komputer telah diperhitungkan. Segala sesuatu yang tertulis dalam teks program, pada akhirnya, turun ke instruksi prosesor. Ini berarti bahwa data yang digunakan oleh program, misalnya, dalam bentuk objek atau tipe data lainnya, juga dikonversi ke struktur tingkat rendah.
Python melakukan transformasi seperti itu secara otomatis, programmer tidak melihat proses ini, dan ia tidak perlu mengurus transformasi seperti itu.
Tidak harus menentukan jenis variabel ketika menyatakan itu bukan fitur bahasa yang membuat Python lambat. Arsitektur bahasa memungkinkan untuk membuat hampir semua hal dinamis. Misalnya, saat dijalankan, Anda dapat mengganti metode objek. Sekali lagi, selama pelaksanaan program, Anda dapat menggunakan teknik "tambalan monyet" sebagaimana diterapkan pada panggilan sistem tingkat rendah. Dengan Python, hampir semuanya mungkin.
Ini adalah arsitektur Python yang membuat optimasi sangat sulit.
Untuk menggambarkan ide ini, saya akan menggunakan alat untuk melacak panggilan sistem di MacOS yang disebut DTrace.
Tidak ada mekanisme dukungan DTrace dalam distribusi CPython yang sudah jadi, jadi CPython perlu dikompilasi ulang dengan pengaturan yang sesuai. Di sini versi 3.6.6 digunakan. Jadi, kami menggunakan urutan tindakan berikut:
wget https://github.com/python/cpython/archive/v3.6.6.zip unzip v3.6.6.zip cd v3.6.6 ./configure --with-dtrace make
Sekarang, menggunakan
python.exe
, Anda dapat menggunakan DTRace untuk melacak kode. Baca tentang menggunakan DTrace dengan Python di
sini . Dan di
sini Anda dapat menemukan skrip untuk mengukur berbagai indikator kinerja program Python menggunakan DTrace. Diantaranya adalah parameter untuk fungsi panggilan, runtime program, waktu penggunaan prosesor, informasi tentang panggilan sistem, dan sebagainya. Inilah cara menggunakan perintah
dtrace
:
sudo dtrace -s toolkit/<tracer>.d -c '../cpython/python.exe script.py'
Dan di sini adalah bagaimana
py_callflow
jejak
py_callflow
menunjukkan pemanggilan fungsi dalam aplikasi.
Melacak Menggunakan DTraceSekarang mari kita jawab pertanyaan apakah pengetikan dinamis mempengaruhi kinerja Python. Inilah beberapa pemikiran tentang ini:
- Pengecekan dan konversi tipe adalah operasi yang berat. Setiap kali variabel diakses, dibaca atau ditulis, pemeriksaan tipe dilakukan.
- Bahasa dengan fleksibilitas seperti itu sulit untuk dioptimalkan. Alasan bahwa bahasa lain jauh lebih cepat daripada Python adalah karena mereka berkompromi dengan memilih antara fleksibilitas dan kinerja.
- Proyek Cython menggabungkan pengetikan Python dan statis, yang, misalnya, seperti yang ditunjukkan dalam artikel ini , mengarah ke peningkatan kinerja 84 kali lipat dari Python biasa. Periksa proyek ini jika Anda membutuhkan kecepatan.
Ringkasan
Alasan buruknya kinerja Python adalah sifat dan fleksibilitasnya yang dinamis. Ini dapat digunakan sebagai alat untuk menyelesaikan berbagai tugas. Untuk mencapai tujuan yang sama, Anda dapat mencoba mencari alat yang lebih produktif dan lebih optimal. Mungkin mereka akan dapat menemukan, mungkin tidak.
Aplikasi yang ditulis dengan Python dapat dioptimalkan menggunakan kemampuan eksekusi kode asinkron, alat profil, dan - memilih juru bahasa yang tepat. Jadi, untuk mengoptimalkan kecepatan aplikasi yang waktu startupnya tidak penting, dan yang kinerjanya mungkin diuntungkan dengan menggunakan kompiler JIT, pertimbangkan untuk menggunakan PyPy. Jika Anda membutuhkan kinerja maksimum dan siap untuk batasan pengetikan statis, lihat Cython.
Pembaca yang budiman! Bagaimana Anda mengatasi masalah kinerja Python yang buruk?
