Jalur untuk memeriksa 4 juta baris kode Python. Bagian 2

Hari ini kami menerbitkan bagian kedua dari terjemahan materi tentang bagaimana Dropbox mengatur kontrol tipe untuk beberapa juta baris kode Python.

Jalur untuk memeriksa 4 juta baris kode Python. Bagian 2

Baca bagian satu

Dukungan tipe resmi (PEP 484)

Kami melakukan eksperimen serius pertama kami dengan mypy di Dropbox selama Hack Week 2014. Hack Week adalah acara satu minggu yang diselenggarakan oleh Dropbox. Selama waktu ini, karyawan dapat mengerjakan apapun yang mereka inginkan! Beberapa proyek teknologi Dropbox yang paling terkenal dimulai pada acara seperti ini. Sebagai hasil percobaan ini, kami menyimpulkan bahwa mypy tampak menjanjikan, meskipun proyek tersebut belum siap untuk digunakan secara luas.

Pada saat itu, gagasan untuk menstandardisasi sistem petunjuk tipe Python sedang mengudara. Seperti yang saya katakan, sejak Python 3.0 dimungkinkan untuk menggunakan anotasi tipe untuk fungsi, tetapi ini hanyalah ekspresi arbitrer, tanpa sintaksis dan semantik yang ditentukan. Selama eksekusi program, sebagian besar anotasi ini diabaikan begitu saja. Setelah Hack Week, kami mulai mengerjakan standarisasi semantik. Pekerjaan ini menyebabkan munculnya PP 484 (Guido van Rossum, Łukasz Langa dan saya berkolaborasi dalam dokumen ini).

Motif kami dapat dilihat dari dua sisi. Pertama, kami berharap seluruh ekosistem Python dapat mengadopsi pendekatan umum dalam menggunakan petunjuk tipe (istilah yang digunakan dalam Python setara dengan "anotasi tipe"). Mengingat risiko yang mungkin terjadi, hal ini akan lebih baik daripada menggunakan banyak pendekatan yang tidak kompatibel satu sama lain. Kedua, kami ingin mendiskusikan mekanisme anotasi tipe secara terbuka dengan banyak anggota komunitas Python. Keinginan ini sebagian ditentukan oleh fakta bahwa kami tidak ingin terlihat seperti "murtad" dari ide dasar bahasa di mata sebagian besar pemrogram Python. Ini adalah bahasa yang diketik secara dinamis, yang dikenal sebagai "pengetikan bebek". Di masyarakat, pada awalnya, sikap yang agak mencurigakan terhadap gagasan pengetikan statis mau tidak mau muncul. Namun sentimen tersebut akhirnya memudar setelah menjadi jelas bahwa pengetikan statis tidak wajib dilakukan (dan setelah orang menyadari bahwa pengetikan statis sebenarnya berguna).

Sintaks petunjuk tipe yang akhirnya diadopsi sangat mirip dengan apa yang didukung mypy pada saat itu. PEP 484 dirilis dengan Python 3.5 pada tahun 2015. Python bukan lagi bahasa yang diketik secara dinamis. Saya menganggap peristiwa ini sebagai tonggak penting dalam sejarah Python.

Mulai dari migrasi

Pada akhir tahun 2015, Dropbox membentuk tim yang terdiri dari tiga orang untuk mengerjakan mypy. Mereka termasuk Guido van Rossum, Greg Price dan David Fisher. Sejak saat itu, situasi mulai berkembang dengan sangat pesat. Hambatan pertama bagi pertumbuhan mypy adalah kinerja. Seperti yang saya sebutkan di atas, pada hari-hari awal proyek saya berpikir untuk menerjemahkan implementasi mypy ke dalam C, tetapi gagasan ini telah dicoret dari daftar untuk saat ini. Kami terjebak dalam menjalankan sistem menggunakan juru bahasa CPython, yang tidak cukup cepat untuk alat seperti mypy. (Proyek PyPy, implementasi Python alternatif dengan kompiler JIT, juga tidak membantu kami.)

Untungnya, beberapa perbaikan algoritmik telah membantu kami di sini. “Akselerator” pertama yang kuat adalah penerapan pemeriksaan bertahap. Ide di balik peningkatan ini sederhana: jika semua dependensi modul tidak berubah sejak mypy dijalankan sebelumnya, maka kita dapat menggunakan data yang di-cache selama proses sebelumnya saat bekerja dengan dependensi. Kami hanya perlu melakukan pemeriksaan tipe pada file yang dimodifikasi dan file yang bergantung padanya. Mypy bahkan melangkah lebih jauh: jika antarmuka eksternal suatu modul tidak berubah, mypy berasumsi bahwa modul lain yang mengimpor modul ini tidak perlu diperiksa lagi.

Pemeriksaan tambahan telah banyak membantu kami saat memberi anotasi pada kode yang ada dalam jumlah besar. Intinya adalah bahwa proses ini biasanya melibatkan banyak proses mypy yang berulang-ulang karena anotasi secara bertahap ditambahkan ke kode dan ditingkatkan secara bertahap. Mypy yang dijalankan pertama kali masih sangat lambat karena memiliki banyak dependensi yang harus diperiksa. Kemudian, untuk memperbaiki situasi ini, kami menerapkan mekanisme cache jarak jauh. Jika mypy mendeteksi bahwa cache lokal kemungkinan sudah kedaluwarsa, mypy akan mengunduh snapshot cache saat ini untuk seluruh basis kode dari repositori terpusat. Kemudian melakukan pemeriksaan tambahan menggunakan snapshot ini. Hal ini membawa kami satu langkah besar lagi menuju peningkatan kinerja mypy.

Ini adalah periode penerapan pengecekan tipe secara cepat dan alami di Dropbox. Pada akhir tahun 2016, kami telah memiliki sekitar 420000 baris kode Python dengan anotasi tipe. Banyak pengguna yang antusias dengan pengecekan tipe. Semakin banyak tim pengembangan yang menggunakan Dropbox mypy.

Semuanya tampak baik-baik saja saat itu, tetapi masih banyak yang harus kami lakukan. Kami mulai melakukan survei pengguna internal secara berkala untuk mengidentifikasi area masalah proyek dan memahami masalah apa yang perlu diselesaikan terlebih dahulu (praktik ini masih digunakan di perusahaan hingga saat ini). Yang paling penting, ternyata, adalah dua tugas. Pertama, kami membutuhkan lebih banyak jenis cakupan kode, kedua, kami membutuhkan mypy agar bekerja lebih cepat. Jelas sekali bahwa pekerjaan kami untuk mempercepat mypy dan mengimplementasikannya ke dalam proyek perusahaan masih jauh dari selesai. Kami, menyadari sepenuhnya pentingnya kedua tugas ini, mulai menyelesaikannya.

Lebih banyak produktivitas!

Pemeriksaan tambahan membuat mypy lebih cepat, namun alat tersebut masih belum cukup cepat. Banyak pemeriksaan tambahan yang berlangsung sekitar satu menit. Alasannya adalah siklus impor. Ini mungkin tidak akan mengejutkan siapa pun yang pernah bekerja dengan basis kode besar yang ditulis dengan Python. Kami memiliki ratusan modul, yang masing-masing mengimpor modul lainnya secara tidak langsung. Jika ada file dalam loop impor yang diubah, mypy harus memproses semua file dalam loop itu, dan seringkali modul apa pun yang mengimpor modul dari loop itu. Salah satu siklus tersebut adalah “kekusutan ketergantungan” yang menyebabkan banyak masalah di Dropbox. Setelah struktur ini berisi beberapa ratus modul, ketika diimpor, secara langsung atau tidak langsung, banyak pengujian, itu juga digunakan dalam kode produksi.

Kami mempertimbangkan kemungkinan untuk "menguraikan" ketergantungan sirkular, namun kami tidak memiliki sumber daya untuk melakukannya. Ada terlalu banyak kode yang tidak kami kenal. Hasilnya, kami menemukan pendekatan alternatif. Kami memutuskan untuk membuat mypy berfungsi dengan cepat bahkan ketika ada “kekusutan ketergantungan”. Kami mencapai tujuan ini menggunakan daemon mypy. Daemon adalah proses server yang mengimplementasikan dua kemampuan menarik. Pertama, ia menyimpan informasi tentang seluruh basis kode di memori. Artinya, setiap kali Anda menjalankan mypy, Anda tidak perlu memuat data cache terkait ribuan dependensi yang diimpor. Kedua, ia dengan hati-hati, pada tingkat unit struktural kecil, menganalisis ketergantungan antara fungsi dan entitas lainnya. Misalnya saja jika fungsinya foo memanggil suatu fungsi bar, maka terjadilah ketergantungan foo dari bar. Ketika sebuah file berubah, daemon pertama-tama, secara terpisah, hanya memproses file yang diubah tersebut. Kemudian melihat perubahan yang terlihat secara eksternal pada file tersebut, seperti perubahan tanda tangan fungsi. Daemon menggunakan informasi rinci tentang impor hanya untuk memeriksa ulang fungsi-fungsi yang benar-benar menggunakan fungsi yang dimodifikasi. Biasanya, dengan pendekatan ini, Anda hanya perlu memeriksa sedikit fungsi.

Mengimplementasikan semua ini tidaklah mudah, karena implementasi mypy asli sangat fokus pada pemrosesan satu file dalam satu waktu. Kami harus menghadapi banyak situasi batas, yang kejadiannya memerlukan pemeriksaan berulang kali jika ada sesuatu yang berubah dalam kode. Misalnya, ini terjadi ketika suatu kelas diberi kelas dasar baru. Setelah kami melakukan apa yang kami inginkan, kami dapat mengurangi waktu eksekusi sebagian besar pemeriksaan tambahan menjadi hanya beberapa detik. Ini tampak seperti kemenangan besar bagi kami.

Produktivitas lebih banyak lagi!

Bersama dengan remote caching yang saya bahas di atas, daemon mypy hampir menyelesaikan sepenuhnya masalah yang muncul ketika seorang programmer sering menjalankan pengecekan tipe, membuat perubahan pada sejumlah kecil file. Namun, performa sistem pada kasus penggunaan yang paling tidak menguntungkan masih jauh dari optimal. Startup mypy yang bersih dapat memakan waktu lebih dari 15 menit. Dan ini jauh lebih dari yang bisa kami senangi. Setiap minggu situasinya menjadi lebih buruk karena pemrogram terus menulis kode baru dan menambahkan anotasi ke kode yang sudah ada. Pengguna kami masih haus akan kinerja yang lebih baik, namun kami senang bisa menemui mereka di tengah jalan.

Kami memutuskan untuk kembali ke salah satu ide sebelumnya mengenai mypy. Yaitu untuk mengubah kode Python menjadi kode C. Bereksperimen dengan Cython (sebuah sistem yang memungkinkan Anda menerjemahkan kode yang ditulis dengan Python ke dalam kode C) tidak memberi kami percepatan yang terlihat, jadi kami memutuskan untuk menghidupkan kembali gagasan untuk menulis kompiler kami sendiri. Karena basis kode mypy (ditulis dengan Python) sudah berisi semua anotasi tipe yang diperlukan, kami pikir akan bermanfaat jika mencoba menggunakan anotasi ini untuk mempercepat sistem. Saya segera membuat prototipe untuk menguji ide ini. Ini menunjukkan peningkatan kinerja lebih dari 10 kali lipat pada berbagai tolok ukur mikro. Ide kami adalah mengkompilasi modul Python ke modul C menggunakan Cython, dan mengubah anotasi tipe menjadi pemeriksaan tipe run-time (biasanya anotasi tipe diabaikan saat run-time dan hanya digunakan oleh sistem pengecekan tipe). Kami sebenarnya berencana untuk menerjemahkan implementasi mypy dari Python ke dalam bahasa yang dirancang untuk diketik secara statis, yang akan terlihat (dan, sebagian besar, berfungsi) persis seperti Python. (Migrasi lintas bahasa semacam ini telah menjadi tradisi proyek mypy. Implementasi mypy asli ditulis dalam Alore, kemudian ada hibrida sintaksis Java dan Python).

Berfokus pada API ekstensi CPython adalah kunci untuk tidak kehilangan kemampuan manajemen proyek. Kami tidak perlu mengimplementasikan mesin virtual atau perpustakaan apa pun yang dibutuhkan mypy. Selain itu, kami masih memiliki akses ke seluruh ekosistem Python dan semua alat (seperti pytest). Ini berarti bahwa kami dapat terus menggunakan kode Python yang diinterpretasikan selama pengembangan, memungkinkan kami untuk terus bekerja dengan pola yang sangat cepat dalam membuat perubahan kode dan mengujinya, daripada menunggu kode dikompilasi. Sepertinya kami melakukan pekerjaan yang baik dengan duduk di dua kursi, dan kami menyukainya.

Kompiler, yang kami sebut mypyc (karena menggunakan mypy sebagai front-end untuk menganalisis tipe), ternyata merupakan proyek yang sangat sukses. Secara keseluruhan, kami mencapai kecepatan sekitar 4x untuk mypy yang sering dijalankan tanpa cache. Mengembangkan inti proyek mypyc membutuhkan tim kecil yang terdiri dari Michael Sullivan, Ivan Levkivsky, Hugh Hahn, dan saya sendiri sekitar 4 bulan kalender. Jumlah pekerjaan ini jauh lebih kecil dibandingkan apa yang diperlukan untuk menulis ulang mypy, misalnya, di C++ atau Go. Dan kami harus membuat lebih sedikit perubahan pada proyek ini daripada yang harus kami lakukan saat menulis ulang proyek tersebut dalam bahasa lain. Kami juga berharap dapat membawa mypyc ke tingkat yang memungkinkan pemrogram Dropbox lain menggunakannya untuk mengkompilasi dan mempercepat kode mereka.

Untuk mencapai tingkat kinerja ini, kami harus menerapkan beberapa solusi teknis yang menarik. Dengan demikian, kompiler dapat mempercepat banyak operasi dengan menggunakan konstruksi C tingkat rendah yang cepat. Misalnya, pemanggilan fungsi yang dikompilasi diterjemahkan menjadi pemanggilan fungsi C. Dan panggilan seperti itu jauh lebih cepat daripada memanggil fungsi yang diinterpretasikan. Beberapa operasi, seperti pencarian kamus, masih melibatkan penggunaan panggilan C-API reguler dari CPython, yang hanya sedikit lebih cepat saat dikompilasi. Kami dapat menghilangkan beban tambahan pada sistem yang disebabkan oleh interpretasi, namun dalam kasus ini hal ini hanya memberikan sedikit keuntungan dalam hal kinerja.

Untuk mengidentifikasi operasi “lambat” yang paling umum, kami melakukan pembuatan profil kode. Berbekal data ini, kami mencoba mengubah mypyc sehingga menghasilkan kode C yang lebih cepat untuk operasi tersebut, atau menulis ulang kode Python yang sesuai menggunakan operasi yang lebih cepat (dan terkadang kami tidak memiliki solusi yang cukup sederhana untuk masalah itu atau masalah lainnya) . Menulis ulang kode Python seringkali merupakan solusi yang lebih mudah untuk masalah ini daripada meminta kompiler melakukan transformasi yang sama secara otomatis. Dalam jangka panjang, kami ingin mengotomatiskan sebagian besar transformasi ini, namun saat itu kami fokus untuk mempercepat mypy dengan sedikit usaha. Dan dalam mencapai tujuan ini, kami mengambil jalan pintas.

Untuk dilanjutkan ...

Pembaca yang terhormat Apa kesan Anda terhadap proyek mypy ketika mengetahui keberadaannya?

Jalur untuk memeriksa 4 juta baris kode Python. Bagian 2
Jalur untuk memeriksa 4 juta baris kode Python. Bagian 2

Sumber: www.habr.com

Tambah komentar