Cinder adalah versi produksi CPython 3.10 yang berorientasi kinerja internal Meta. Ini berisi sejumlah optimasi kinerja, termasuk cache inline bytecode, evaluasi coroutine yang bersemangat, JIT metode-pada-waktu, dan kompiler bytecode eksperimental yang menggunakan anotasi tipe untuk memancarkan bytecode khusus tipe yang berkinerja lebih baik di JIT.
Cinder memberdayakan Instagram, tempat ia dimulai, dan semakin banyak digunakan di lebih banyak aplikasi Python di Meta.
Untuk informasi selengkapnya tentang CPython, lihat README.cpython.rst
.
Jawaban singkatnya: tidak.
Kami telah membuat Cinder tersedia untuk umum untuk memfasilitasi percakapan tentang potensi upstreaming beberapa pekerjaan ini ke CPython dan untuk mengurangi duplikasi upaya di antara orang-orang yang mengerjakan kinerja CPython.
Cinder tidak dipoles atau didokumentasikan untuk digunakan orang lain. Kami tidak memiliki keinginan untuk menjadi alternatif dari CPython. Tujuan kami dalam menyediakan kode ini adalah CPython terpadu yang lebih cepat. Jadi meskipun kami menjalankan Cinder dalam produksi, jika Anda memilih untuk melakukannya, Anda harus melakukannya sendiri. Kami tidak dapat berkomitmen untuk memperbaiki laporan bug eksternal atau meninjau permintaan penarikan. Kami memastikan Cinder cukup stabil dan cepat untuk beban kerja produksi kami, namun kami tidak memberikan jaminan mengenai stabilitas atau kebenaran atau kinerjanya untuk beban kerja eksternal atau kasus penggunaan apa pun.
Oleh karena itu, jika Anda memiliki pengalaman dalam runtime bahasa dinamis dan memiliki ide untuk membuat Cinder lebih cepat; atau jika Anda mengerjakan CPython dan ingin menggunakan Cinder sebagai inspirasi untuk perbaikan di CPython (atau membantu bagian hulu Cinder ke CPython), silakan hubungi; kami ingin mengobrol!
Cinder harus dibuat seperti CPython; configure
dan make -j
. Namun karena sebagian besar pengembangan dan penggunaan Cinder terjadi dalam konteks Meta yang sangat spesifik, kami tidak banyak menerapkannya di lingkungan lain. Oleh karena itu, cara paling andal untuk membangun dan menjalankan Cinder adalah dengan menggunakan kembali pengaturan berbasis Docker dari alur kerja GitHub CI kami.
Jika Anda hanya ingin mendapatkan Cinder yang berfungsi tanpa membuatnya sendiri, Runtime Docker Image kami akan menjadi yang termudah (tidak perlu kloning repo!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
Jika Anda ingin membuatnya sendiri:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
Perlu diketahui bahwa Cinder hanya dibuat atau diuji di Linux x64; hal lain (termasuk macOS) mungkin tidak akan berfungsi. Gambar Docker di atas berbasis Fedora Linux dan dibuat dari file spesifikasi Docker di repo Cinder: .github/workflows/python-build-env/Dockerfile
.
Ada beberapa target pengujian baru yang mungkin menarik. make testcinder
hampir sama dengan make test
hanya saja ia melewatkan beberapa pengujian yang bermasalah di lingkungan pengembang kita. make testcinder_jit
menjalankan rangkaian pengujian dengan JIT diaktifkan sepenuhnya, sehingga semua fungsi menggunakan JIT. make testruntime
menjalankan rangkaian pengujian unit C++ gtest untuk JIT. Dan make test_strict_module
menjalankan rangkaian pengujian untuk modul yang ketat (lihat di bawah).
Perhatikan bahwa langkah-langkah ini menghasilkan biner Cinder Python tanpa mengaktifkan pengoptimalan PGO/LTO, jadi jangan berharap untuk menggunakan petunjuk ini untuk mempercepat beban kerja Python apa pun.
Cinder Explorer adalah taman bermain langsung, di mana Anda dapat melihat bagaimana Cinder mengkompilasi kode Python dari sumber ke perakitan -- silakan mencobanya! Jangan ragu untuk mengajukan permintaan fitur dan laporan bug. Ingatlah bahwa Cinder Explorer, seperti yang lainnya, "didukung" berdasarkan upaya terbaik.
Instagram menggunakan arsitektur server web multi-proses; proses induk dimulai, melakukan pekerjaan inisialisasi (misalnya memuat kode), dan membagi puluhan proses pekerja untuk menangani permintaan klien. Proses pekerja dimulai ulang secara berkala karena sejumlah alasan (misalnya kebocoran memori, penerapan kode) dan memiliki masa pakai yang relatif singkat. Dalam model ini, OS harus menyalin seluruh halaman yang berisi objek yang dialokasikan dalam proses induk ketika jumlah referensi objek diubah. Dalam praktiknya, objek yang dialokasikan dalam proses induk hidup lebih lama dari pekerja; semua pekerjaan yang berhubungan dengan penghitungan referensi tidak diperlukan.
Instagram memiliki basis kode Python yang sangat besar dan overhead akibat copy-on-write dari referensi penghitungan objek berumur panjang ternyata signifikan. Kami mengembangkan solusi yang disebut "instans abadi" untuk menyediakan cara untuk mengecualikan objek dari penghitungan referensi. Lihat Sertakan/objek.h untuk detailnya. Fitur ini dikontrol dengan mendefinisikan Py_IMMORTAL_INSTANCES dan diaktifkan secara default di Cinder. Ini merupakan kemenangan besar bagi kami dalam produksi (~5%), namun membuat kode garis lurus menjadi lebih lambat. Operasi penghitungan referensi sering terjadi dan harus memeriksa apakah suatu objek berpartisipasi dalam penghitungan referensi atau tidak ketika fitur ini diaktifkan.
"Shadowcode" atau "shadow bytecode" adalah implementasi penerjemah khusus kami. Ia mengamati kasus-kasus tertentu yang dapat dioptimalkan dalam eksekusi opcode Python generik dan (untuk fungsi-fungsi panas) secara dinamis mengganti opcode-opcode tersebut dengan versi khusus. Inti dari shadowcode ada di Shadowcode/shadowcode.c
, meskipun implementasi untuk bytecode khusus ada di Python/ceval.c
dengan loop eval lainnya. Tes khusus kode bayangan ada di Lib/test/test_shadowcode.py
.
Semangatnya serupa dengan penerjemah adaptif khusus (PEP-659) yang akan dibangun di CPython 3.11.
Server Instagram merupakan beban kerja asinkron, di mana setiap permintaan web dapat memicu ratusan ribu tugas asinkron, banyak di antaranya dapat diselesaikan tanpa penangguhan (misalnya berkat nilai memo).
Kami memperluas protokol panggilan vektor untuk meneruskan tanda baru, Ci_Py_AWAITED_CALL_MARKER
, yang menunjukkan bahwa penelepon segera menunggu panggilan ini.
Saat digunakan dengan pemanggilan fungsi async yang segera ditunggu, kita bisa langsung (dengan penuh semangat) mengevaluasi fungsi yang dipanggil, hingga selesai, atau hingga penangguhan pertamanya. Jika fungsi selesai tanpa penangguhan, kami dapat segera mengembalikan nilainya, tanpa alokasi heap tambahan.
Saat digunakan dengan pengumpulan async, kita dapat segera (dengan penuh semangat) mengevaluasi kumpulan menunggu yang telah dilewati, berpotensi menghindari biaya pembuatan dan penjadwalan beberapa tugas untuk coroutine yang dapat diselesaikan secara sinkron, penyelesaian masa depan, nilai memo, dll.
Pengoptimalan ini menghasilkan peningkatan efisiensi CPU yang signifikan (~5%).
Hal ini sebagian besar diimplementasikan dalam Python/ceval.c
, melalui flag panggilan vektor baru Ci_Py_AWAITED_CALL_MARKER
, yang menunjukkan bahwa penelepon segera menunggu panggilan ini. Carilah penggunaan makro IS_AWAITED()
dan tanda panggilan vektor ini.
Cinder JIT adalah JIT kustom metode-pada-waktu yang diimplementasikan dalam C++. Ini diaktifkan melalui flag -X jit
atau variabel lingkungan PYTHONJIT=1
. Ini mendukung hampir semua opcode Python, dan dapat mencapai peningkatan kecepatan 1,5-4x pada banyak tolok ukur kinerja Python.
Secara default, ketika diaktifkan, program ini akan mengkompilasi JIT setiap fungsi yang pernah dipanggil, yang mungkin membuat program Anda lebih lambat, bukan lebih cepat, karena overhead kompilasi JIT dari fungsi-fungsi yang jarang dipanggil. Opsi -X jit-list-file=/path/to/jitlist.txt
atau PYTHONJITLISTFILE=/path/to/jitlist.txt
dapat mengarahkannya ke file teks yang berisi nama fungsi yang sepenuhnya memenuhi syarat (dalam bentuk path.to.module:funcname
atau path.to.module:ClassName.method_name
), satu per baris, yang harus dikompilasi JIT. Kami menggunakan opsi ini untuk mengkompilasi hanya sekumpulan fungsi panas yang berasal dari data profil produksi. (Pendekatan yang lebih umum untuk JIT adalah mengkompilasi fungsi secara dinamis karena fungsi tersebut sering dipanggil. Kami masih belum layak untuk mengimplementasikannya, karena arsitektur produksi kami adalah server web pre-fork, dan untuk alasan berbagi memori kami ingin melakukan semua kompilasi JIT kami terlebih dahulu dalam proses awal sebelum pekerja di-fork, yang berarti kami tidak dapat mengamati beban kerja dalam proses sebelum memutuskan fungsi mana yang akan dikompilasi JIT.)
JIT berada di direktori Jit/
, dan pengujian C++-nya berada di RuntimeTests/
(jalankan dengan make testruntime
). Ada juga beberapa tes Python untuk itu di Lib/test/test_cinderjit.py
; ini tidak dimaksudkan untuk menyeluruh, karena kami menjalankan seluruh rangkaian pengujian CPython di bawah JIT melalui make testcinder_jit
; mereka mencakup kasus tepi JIT yang tidak ditemukan di rangkaian pengujian CPython.
Lihat Jit/pyjit.cpp
untuk beberapa opsi -X
dan variabel lingkungan lainnya yang memengaruhi perilaku JIT. Ada juga modul cinderjit
yang didefinisikan dalam file tersebut yang mengekspos beberapa utilitas JIT ke kode Python (misalnya memaksa fungsi tertentu untuk dikompilasi, memeriksa apakah suatu fungsi dikompilasi, menonaktifkan JIT). Perhatikan bahwa cinderjit.disable()
hanya menonaktifkan kompilasi di masa mendatang; itu segera mengkompilasi semua fungsi yang diketahui dan mempertahankan fungsi-fungsi yang dikompilasi JIT yang ada.
JIT pertama-tama menurunkan bytecode Python ke representasi perantara tingkat tinggi (HIR); ini diimplementasikan di Jit/hir/
. Peta HIR cukup dekat dengan bytecode Python, meskipun ini adalah mesin register dan bukan mesin tumpukan, levelnya sedikit lebih rendah, diketik, dan beberapa detail yang dikaburkan oleh bytecode Python tetapi penting untuk kinerja (terutama penghitungan referensi) adalah diekspos secara eksplisit di HIR. HIR diubah menjadi bentuk SSA, beberapa lintasan optimasi dilakukan padanya, dan kemudian operasi penghitungan referensi secara otomatis dimasukkan ke dalamnya sesuai dengan metadata tentang penghitungan ulang dan efek memori dari opcode HIR.
HIR kemudian diturunkan ke representasi perantara tingkat rendah (LIR), yang merupakan abstraksi atas perakitan, diimplementasikan dalam Jit/lir/
. Di LIR kami melakukan alokasi register, beberapa lintasan optimasi tambahan, dan akhirnya LIR diturunkan ke perakitan (dalam Jit/codegen/
) menggunakan perpustakaan asmjit yang sangat baik.
JIT sedang dalam tahap awal. Meskipun sudah dapat menghilangkan overhead loop interpreter dan menawarkan peningkatan kinerja yang signifikan untuk banyak fungsi, kami baru mulai menggali kemungkinan pengoptimalannya. Banyak optimasi kompiler umum yang belum diterapkan. Prioritas kami terhadap pengoptimalan sebagian besar didorong oleh karakteristik beban kerja produksi Instagram.
Modul yang ketat adalah beberapa hal yang digabungkan menjadi satu:
1. Penganalisis statis yang mampu memvalidasi bahwa mengeksekusi kode tingkat atas modul tidak akan menimbulkan efek samping yang terlihat di luar modul tersebut.
2. Tipe StrictModule
yang tidak dapat diubah dan dapat digunakan sebagai pengganti tipe modul default Python.
3. Pemuat modul Python yang mampu mengenali modul yang ikut serta dalam mode ketat (melalui import __strict__
di bagian atas modul), menganalisisnya untuk memvalidasi tanpa efek samping impor, dan mengisinya di sys.modules
sebagai objek StrictModule
.
Static Python adalah kompiler bytecode yang menggunakan anotasi tipe untuk memancarkan bytecode Python yang terspesialisasi dan tipe yang diperiksa. Digunakan bersama dengan Cinder JIT, ia dapat memberikan kinerja yang mirip dengan MyPyC atau Cython dalam banyak kasus, sekaligus menawarkan pengalaman pengembang Python murni (sintaks Python normal, tanpa langkah kompilasi tambahan). Static Python plus Cinder JIT mencapai 18x kinerja stock CPython pada versi benchmark Richards yang diketik. Di Instagram kami telah berhasil menggunakan Static Python dalam produksi untuk menggantikan semua modul Cython di basis kode server web utama kami, tanpa regresi kinerja.
Kompiler Python Statis dibangun di atas modul compiler
Python yang telah dihapus dari perpustakaan standar di Python 3 dan sejak itu dipelihara dan diperbarui secara eksternal; kompiler ini dimasukkan ke dalam Cinder di Lib/compiler
. Kompiler Static Python diimplementasikan di Lib/compiler/static/
, dan pengujiannya dilakukan di Lib/test/test_compiler/test_static.py
.
Kelas yang didefinisikan dalam modul Python Statis secara otomatis diberikan slot yang diketik (berdasarkan pemeriksaan atribut kelas yang diketik dan tugas yang dianotasi di __init__
), dan pemuatan serta penyimpanan atribut terhadap instance jenis ini menggunakan opcode STORE_FIELD
dan LOAD_FIELD
baru, yang di JIT menjadi langsung memuat/menyimpan dari/ke offset memori tetap dalam objek, tanpa tipuan LOAD_ATTR
atau STORE_ATTR
. Kelas juga mendapatkan tabel v dari metodenya, untuk digunakan oleh opcode INVOKE_*
yang disebutkan di bawah. Dukungan runtime untuk fitur ini terletak di StaticPython/classloader.h
dan StaticPython/classloader.c
.
Fungsi Python statis dimulai dengan prolog tersembunyi yang memeriksa apakah tipe argumen yang diberikan cocok dengan anotasi tipe, dan memunculkan TypeError
jika tidak. Panggilan dari fungsi Python statis ke fungsi Python statis lainnya akan melewati opcode ini (karena tipenya sudah divalidasi oleh kompiler). Panggilan statis ke statis juga dapat menghindari banyak overhead dari panggilan fungsi Python pada umumnya. Kami mengeluarkan opcode INVOKE_FUNCTION
atau INVOKE_METHOD
yang membawa serta metadata tentang fungsi atau metode yang dipanggil; ini ditambah modul opsional yang tidak dapat diubah (melalui StrictModule
) dan tipe (melalui cinder.freeze_type()
, yang saat ini kami terapkan ke semua tipe dalam modul ketat dan statis di pemuat impor kami, tetapi di masa mendatang mungkin menjadi bagian yang melekat dari Static Python) dan kompilasi Pengetahuan -time tentang tanda tangan callee memungkinkan kita untuk (di JIT) mengubah banyak panggilan fungsi Python menjadi panggilan langsung ke alamat memori tetap menggunakan konvensi pemanggilan x64, dengan overhead yang lebih sedikit daripada panggilan fungsi C.
Python Statis masih diketik secara bertahap, dan mendukung kode yang hanya dianotasi sebagian atau menggunakan tipe yang tidak diketahui dengan kembali ke perilaku dinamis Python normal. Dalam beberapa kasus (misalnya ketika nilai tipe yang tidak diketahui secara statis dikembalikan dari fungsi dengan anotasi pengembalian), opcode CAST
runtime dimasukkan yang akan memunculkan TypeError
jika tipe runtime tidak cocok dengan tipe yang diharapkan.
Static Python juga mendukung tipe baru untuk integer mesin, bools, doubles, dan vektor/array. Dalam JIT ini ditangani sebagai nilai-nilai yang tidak dimasukkan ke dalam kotak, dan misalnya aritmatika bilangan bulat primitif menghindari semua overhead Python. Beberapa operasi pada tipe bawaan (misalnya subskrip daftar atau kamus atau len()
) juga dioptimalkan.
Cinder mendukung adopsi modul statis secara bertahap melalui pemuat modul ketat/statis yang dapat secara otomatis mendeteksi modul statis dan memuatnya sebagai modul statis dengan kompilasi lintas modul. Loader akan mencari anotasi import __static__
dan import __strict__
di bagian atas file, dan mengkompilasi modul dengan tepat. Untuk mengaktifkan pemuat, Anda memiliki salah satu dari tiga opsi:
1. Instal loader secara eksplisit di tingkat atas aplikasi Anda melalui from cinderx.compiler.strict.loader import install; install()
.
PYTHONINSTALLSTRICTLOADER=1
di env../python -X install-strict-loader application.py
. Alternatifnya, Anda dapat mengkompilasi semua kode secara statis dengan menggunakan ./python -m compiler --static some_module.py
, yang akan mengkompilasi modul sebagai Python statis dan menjalankannya.
Lihat CinderDoc/static_python.rst
untuk dokumentasi lebih detail.