Hal paling menarik di PHP 8

PHP 7.4 baru saja dinyatakan stabil, dan kami telah mengirimkan lebih banyak perbaikan. Dan yang terpenting, apa yang ditunggu-tunggu PHP dapat memberi tahu Dmitry Stogov - salah satu pengembang terkemuka Open Source PHP dan, mungkin, kontributor aktif tertua.

Semua laporan Dmitry hanya tentang teknologi dan solusi yang ia kerjakan secara pribadi. Dalam tradisi terbaik Ontiko, di bawah potongan, versi teks cerita tentang inovasi PHP 8 paling menarik dari sudut pandang Dmitry yang dapat membuka kasus penggunaan baru. Pertama-tama, JIT dan FFI - bukan kunci โ€œprospek luar biasaโ€, tetapi dengan detail implementasi dan jebakan.


Untuk referensi: Dmitry Stogov berkenalan dengan pemrograman pada tahun 1984, ketika tidak semua pembaca lahir, dan berhasil memberikan kontribusi yang signifikan terhadap pengembangan alat pengembangan, dan PHP khususnya (meskipun Dmitry meningkatkan kinerja PHP tidak khusus untuk pengembang Rusia, mereka menyatakan terima kasih saya dalam bentuk Penghargaan HighLoad ++). Dmitry adalah penulis Turck MMCache untuk PHP (eAccelerator), pengelola Zend OPcache, pemimpin proyek PHPNG, yang membentuk dasar dari PHP 7, dan pemimpin dalam pengembangan JIT untuk PHP.

Pengembangan kinerja PHP


Saya mulai bekerja pada kinerja PHP 15 tahun yang lalu ketika saya bergabung dengan Zend. Lalu kami merilis versi 5.0 - yang pertama di mana bahasa menjadi benar-benar berorientasi objek. Sejak itu, kami telah mampu meningkatkan kinerja pada tes sintetik sebanyak 40 kali, dan pada aplikasi nyata sebanyak 6 kali.



Selama waktu ini, ada dua momen terobosan:

  • Versi 5.1, di mana kami dapat secara signifikan meningkatkan kecepatan interpretasi. Kami menerapkan juru bahasa khusus, dan ini terutama mempengaruhi tes sintetik.
  • Versi 7.0, di mana semua struktur data utama diproses dan dengan demikian mengoptimalkan pekerjaan dengan memori dan cache prosesor (baca lebih lanjut tentang optimasi ini di sini ). Hal ini menyebabkan percepatan lebih dari dua kali lipat baik dalam tes sintetik dan aplikasi nyata.

Semua versi lain secara bertahap meningkatkan produktivitas dengan menerapkan banyak ide yang kurang efektif. Dalam versi 7.1, misalnya, banyak perhatian diberikan untuk mengoptimalkan bytecode ( sebuah artikel tentang solusi ini).

Diagram menunjukkan bahwa baik pada akhir pengembangan versi 5 dan pada akhir siklus pengembangan versi 7, kita pergi ke dataran tinggi dan melambat. Jadi selama tahun terakhir bekerja pada v7.4, hanya peningkatan 2% dalam produktivitas yang telah dicapai. Dan ini tidak buruk, karena fitur-fitur baru seperti properti yang diketik dan tipe kovarian telah muncul yang memperlambat PHP (Nikita Popov berbicara tentang produk-produk baru ini di PHP Rusia).

Dan sekarang semua orang bertanya-tanya apa yang diharapkan dari versi ke-8, dapatkah ia mengulang kesuksesan v7?

Ke JIT atau tidak ke JIT


Gagasan untuk meningkatkan penerjemah belum habis, tetapi semuanya membutuhkan penelitian yang sangat substansial. Banyak dari mereka harus ditolak pada tahap pembuktian konsep, karena perolehan yang dapat diperoleh ternyata tidak dapat dibandingkan dengan kerumitan atau batasan teknis yang dipaksakan.

Tetapi masih ada harapan untuk teknologi terobosan baru - tentu saja, saya ingat JIT dan kisah sukses mesin JavaScript.

Bahkan, bekerja pada JIT untuk PHP telah berlangsung sejak 2012. Ada 3 atau 4 implementasi, kami bekerja dengan kolega Intel, peretas JavaScript, tetapi entah bagaimana itu tidak mungkin untuk memasukkan JIT di cabang utama. Pada akhirnya, di PHP 8, kami memasukkan JIT dalam kompiler dan melihat akselerasi ganda, tetapi hanya pada tes sintetik, tetapi pada aplikasi nyata, sebaliknya, perlambatan.



Tentu saja, ini bukan yang kita perjuangkan.

Ada apa? Mungkin kita melakukan sesuatu yang salah, mungkin WordPress sangat buruk, dan tidak ada JIT yang akan membantunya (ya, sebenarnya itu). Mungkin kita sudah membuat penerjemah terlalu bagus, tetapi dalam JavaScript itu lebih buruk. Dalam tes komputasi, ini benar: penerjemah PHP adalah salah satu yang terbaik .



Pada tes Mandelbrot, ia bahkan menyalip permata seperti LuaJIT, seorang penerjemah yang ditulis dalam bahasa assembly. Dalam tes ini, kita hanya 4 kali di belakang kompiler GCC-5.3 yang mengoptimalkan. Dengan JIT, kami bisa mendapatkan hasil yang lebih baik dalam tes Mandelbrot. Sebenarnya, kami sudah melakukan ini, yaitu, kami dapat menghasilkan kode yang bersaing dengan kompiler C.

Lalu mengapa kita tidak dapat mempercepat aplikasi nyata? Untuk memahami, saya akan memberi tahu Anda bagaimana kami melakukan JIT. Mari kita mulai dengan dasar-dasarnya.

Bagaimana cara kerja PHP



Server menerima permintaan, mengkompilasinya menjadi bytecode, yang pada gilirannya dikirim ke mesin virtual untuk dieksekusi. Dengan menjalankan bytecode, mesin virtual juga dapat memanggil file PHP lainnya, yang sekali lagi dikompilasi ulang menjadi bytecode dan dieksekusi lagi.

Setelah menyelesaikan kueri, semua informasi yang berkaitan dengannya, termasuk kode byte, dihapus dari memori. Artinya, setiap skrip PHP harus dikompilasi pada setiap permintaan lagi. Tentu saja, tidak mungkin untuk menanamkan kompilasi JIT ke dalam skema seperti itu, karena kompiler harus sangat cepat.

Tetapi kemungkinan besar tidak ada yang menggunakan PHP dalam bentuk aslinya, semua orang menggunakannya dengan OPcache.

PHP + OPcache



Tujuan utama OPcache adalah untuk menyingkirkan kompilasi ulang skrip pada setiap permintaan. Itu tertanam di titik yang dirancang khusus untuk itu, mencegat semua permintaan kompilasi dan cache bytecode yang dikompilasi dalam memori bersama.

Pada saat yang sama, tidak hanya waktu kompilasi yang disimpan, tetapi juga memori, karena memori bytecode sebelumnya dialokasikan di ruang alamat setiap proses, dan sekarang ada dalam satu salinan.

Anda sudah dapat menanamkan JIT di sirkuit ini, yang akan kami lakukan. Tapi pertama-tama, saya akan menunjukkan kepada Anda bagaimana penerjemah bekerja.



Seorang juru bahasa pertama-tama adalah sebuah loop yang memanggil penangannya sendiri untuk setiap instruksi.

Kami menggunakan dua register:

  • execute_data - penunjuk ke bingkai aktivasi saat ini;
  • opline - penunjuk ke instruksi virtual yang dapat dieksekusi saat ini.

Menggunakan ekstensi gcc, kedua jenis register ini memetakan ke register perangkat keras yang sebenarnya, dan karena itu mereka bekerja dengan sangat cepat.

Dalam loop, kita cukup memanggil pawang untuk setiap instruksi, setelah itu pada akhir setiap pawang kita memindahkan pointer ke instruksi berikutnya.

Penting untuk dicatat bahwa alamat pawang ditulis langsung ke bytecode. Mungkin ada beberapa penangan yang berbeda untuk satu instruksi. Ini awalnya diciptakan untuk spesialisasi sehingga penangan dapat berspesialisasi dalam jenis operan. Teknologi yang sama digunakan untuk JIT, karena jika Anda menulis alamat ke kode baru yang dihasilkan sebagai penangan, maka penangan-JIT akan diluncurkan tanpa perubahan apa pun pada juru bahasa.

Pada contoh di atas, pawang yang ditulis untuk instruksi tambahan disajikan di sebelah kanan. Dibutuhkan operan (di sini yang pertama dan kedua dapat berupa variabel konstan, sementara atau lokal), membaca operan, memeriksa jenis, menghasilkan logika langsung - tambahan - dan kemudian kembali ke loop, yang mentransfer kontrol ke pengendali berikutnya.

Fungsi khusus dihasilkan dari deskripsi ini. Karena ada tiga kemungkinan operan pertama, tiga kemungkinan kedua, kami mendapatkan 9 fungsi yang berbeda.



Dalam fungsi-fungsi ini, alih-alih metode universal untuk mendapatkan operan, yang spesifik digunakan yang tidak melakukan pemeriksaan.

Mesin Virtual Hibrid


Komplikasi lain yang kami buat dalam versi 7.2 adalah apa yang disebut mesin virtual hybrid.

Jika sebelumnya kita selalu memanggil pawang menggunakan panggilan tidak langsung langsung di loop juru bahasa, sekarang untuk masing-masing pawang kita juga memasukkan label di tubuh loop, yang kita lompat untuk menggunakan lompatan tidak langsung dan di mana kita memanggil handler itu sendiri, tetapi secara langsung.



Tampaknya sebelumnya mereka membuat satu panggilan tidak langsung, sekarang dua: transisi tidak langsung dan panggilan langsung, dan sistem seperti itu seharusnya bekerja lebih lambat. Tetapi sebenarnya ini bekerja lebih cepat, karena kami membantu prosesor untuk memprediksi transisi. Sebelumnya, ada satu titik dari mana transisi ke tempat yang berbeda dilakukan. Prosesornya sering keliru, karena ia tidak dapat mengingat bahwa ia perlu melompat terlebih dahulu pada satu instruksi, kemudian pada instruksi lainnya. Sekarang, setelah setiap panggilan langsung, ada transisi tidak langsung ke label berikutnya. Akibatnya, ketika loop PHP dieksekusi, instruksi PHP virtual diatur dalam urutan stabil, yang kemudian dieksekusi hampir secara linear.

Mesin virtual hybrid memungkinkan untuk meningkatkan produktivitas sebesar 5-10%.

PHP + OPcache + JIT


JIT diimplementasikan sebagai bagian dari OPcache.



Setelah bytecode dikompilasi dan dioptimalkan, kompiler JIT diluncurkan untuknya, yang tidak lagi berfungsi dengan kode sumber. Dari bytecode PHP, kompiler JIT menghasilkan kode asli, setelah itu alamat instruksi pertama (sebenarnya fungsi) diubah dalam bytecode.

Setelah itu, kode asli yang sudah dibuat mulai dipanggil dari interpreter yang ada tanpa perubahan apa pun. Saya akan menunjukkan kepada Anda contoh sederhana.



Di sebelah kiri, fungsi tertentu ditulis dalam PHP yang menghitung jumlah angka dari 0 hingga 100. Di sebelah kanan, bytecode yang dihasilkan. Instruksi pertama memberikan 0 ke jumlah, yang kedua melakukan hal yang sama untuk i, kemudian lompatan tanpa syarat ke label. Pada label L1, kondisi untuk keluar dari siklus diperiksa: jika sudah terpenuhi, kemudian keluar, jika tidak, kemudian pergi ke siklus. Selanjutnya, tambahkan ke jumlah i, tulis hasilnya dalam jumlah, tambah i dengan 1.

Langsung dari sini kita menghasilkan kode assembler, yang ternyata cukup bagus.



Instruksi QM_ASSIGN pertama dikompilasi menjadi hanya dua instruksi mesin (2-3 baris). Register %esi berisi pointer ke bingkai aktivasi saat ini. Pada offset 30 terletak jumlah variabel. Instruksi pertama menulis nilai 0, yang kedua menulis 4 - ini adalah pengidentifikasi tipe integer ( IS_LONG ). Untuk variabel i kompiler menyadari bahwa ia selalu panjang, dan untuk itu tidak perlu menyimpan jenisnya. Selain itu, dapat disimpan dalam register mesin. Oleh karena itu, di sini hanya XOR dari register itu sendiri adalah instruksi paling sederhana dan termurah untuk mengatur ulang.

Kemudian, dengan cara yang sama, transisi tanpa syarat, kami memeriksa apakah beberapa peristiwa eksternal telah terjadi, kami memeriksa kondisi siklus, kami masuk ke siklus. Pengulangan memeriksa apakah jumlah adalah bilangan bulat: jika ya, kemudian baca nilai bilangan bulat, tambahkan nilai i ke dalamnya, periksa apakah terjadi overflow, tulis kembali hasilnya ke jumlah tersebut dan tambahkan 1 ke %edx .

Dapat dilihat bahwa kodenya mendekati optimal. Akan lebih mungkin untuk mengoptimalkannya, menyingkirkan memeriksa jumlah untuk jenis di setiap iterasi dari loop. Tapi ini sudah optimasi yang cukup rumit, kami belum melakukannya. Kami sedang mengembangkan JIT sebagai teknologi yang cukup sederhana , kami tidak mencoba melakukan apa yang Java HotSpot coba lakukan, V8 - kami memiliki daya yang lebih kecil.

Apa yang salah dengan jit


Mengapa, dengan kode assembler yang begitu bagus, tidak bisakah kita mempercepat aplikasi nyata?

Sebenarnya, haruskah mereka?

  • Jika bottleneck tidak ada dalam CPU, maka JIT tidak akan membantu.
  • Terlalu banyak kode yang dihasilkan (kode mengasapi).
  • Inferensi tipe statis tidak selalu berhasil.
  • Kode jujur โ€‹โ€‹(untuk kasus yang tidak pernah dieksekusi).
  • Dukungan untuk kondisi konsisten mesin virtual (dan tiba-tiba pengecualian).
  • Kelas hidup hanya untuk satu permintaan.

Jika aplikasi menunggu 80% dari waktu untuk tanggapan dari database, maka JIT tidak akan membantu. Jika kita memanggil fungsi intensif sumber daya eksternal, misalnya, cocok dengan ekspresi reguler, maka JIT juga akan memanggil fungsi yang sama dengan cara yang sama. Selain itu, jika suatu aplikasi membangun struktur data besar - pohon, grafik, dan kemudian membacanya, maka menggunakan JIT kita menghasilkan kode yang akan membaca dalam instruksi yang lebih sedikit, tetapi untuk memuat data itu sendiri, itu akan memakan waktu yang sama, tetapi Anda juga perlu memuat kode.

Seperti yang telah Anda lihat, JIT bahkan dapat memperlambat aplikasi nyata, karena menghasilkan banyak kode dan membacanya menjadi masalah - ketika membaca kode dalam jumlah besar, data lain dipaksa keluar dari cache, yang mengarah ke perlambatan.

Paket sederhana untuk PHP 8


Salah satu perbaikan yang ingin kami capai dalam PHP 8 adalah menghasilkan lebih sedikit kode . Sekarang, seperti yang saya katakan, kami membuat kode asli untuk seluruh skrip, yang kami muat pada tahap pemuatan. Tetapi setengah dari fungsi tentu tidak akan dipanggil. Jadi kami melangkah lebih jauh dan memperkenalkan pemicu yang memungkinkan kami untuk mengonfigurasi saat kami ingin menjalankan JIT. Itu bisa dijalankan:

  • untuk semua fungsi;
  • hanya untuk fungsi saat pertama kali dipanggil;
  • Anda dapat menggantung penghitung di setiap fungsi dan mengkompilasi hanya fungsi-fungsi yang benar-benar panas.

Skema seperti itu mungkin bekerja sedikit lebih baik, tetapi masih belum optimal, karena sekali lagi di setiap fungsi ada jalur yang dieksekusi, dan jalur yang tidak pernah dieksekusi. Karena PHP adalah bahasa pemrograman yang dinamis, yaitu, setiap variabel dapat memiliki jenis yang berbeda, ternyata Anda perlu mendukung semua jenis yang diprediksi oleh penganalisa statis. Dan dia sering melakukan ini dengan hati-hati ketika dia tidak dapat membuktikan bahwa tipe yang lain tidak bisa melakukannya.

Dalam kondisi ini, kita akan menjauh dari kompilasi yang jujur โ€‹โ€‹dan mulai melakukannya secara spekulatif.



Di masa depan, kami berencana untuk pertama kali selama beberapa waktu selama pekerjaan aplikasi menganalisis fungsi "terpanas", melihat jalur apa yang dilalui program, jenis variabel apa, bahkan mungkin mengingat kondisi batas, dan hanya kemudian menghasilkan kode fungsi yang optimal untuk saat ini cara eksekusi - hanya untuk bagian-bagian yang benar-benar dieksekusi.

Untuk yang lainnya, kami akan meletakkan bertopik. Semua sama, akan ada pemeriksaan dan kemungkinan keluaran di mana proses deoptimisasi akan dimulai, yaitu, kami akan mengembalikan keadaan mesin virtual yang diperlukan untuk interpretasi dan memberikannya kepada penerjemah untuk dieksekusi.

Skema serupa digunakan di HotSpot Java VM dan V8. Tetapi mengadaptasi teknologi ke PHP memiliki sejumlah kesulitan. Pertama-tama, ini adalah kami telah membagikan bytecode dan membagikan kode asli yang digunakan dari berbagai proses. Kami tidak dapat mengubahnya secara langsung dalam memori bersama, pertama-tama kami harus menyalin di suatu tempat, mengubah, dan kemudian melakukan kembali ke memori bersama.

Preloading. Masalah pengikatan kelas


Bahkan, banyak ide untuk peningkatan PHP yang telah lama disertakan dalam PHP 7 dan bahkan PHP 5 berasal dari pekerjaan yang berhubungan dengan JIT. Hari ini saya akan berbicara tentang teknologi lain seperti ini - ini preloading. Teknologi ini sudah termasuk dalam PHP 7.4 dan memungkinkan untuk menentukan satu set file, memuatnya saat server mulai, dan menjadikan semua fungsi file-file ini permanen.

Salah satu masalah yang memecahkan teknologi preloading adalah masalah pengikatan kelas. Faktanya adalah ketika kita hanya mengkompilasi file dalam PHP, setiap file dikompilasi secara terpisah dari yang lain. Ini dilakukan karena masing-masing dapat diubah secara terpisah. Anda tidak dapat mengaitkan kelas dari satu skrip dengan kelas dari skrip lain, karena pada permintaan berikutnya salah satu dari mereka dapat berubah, dan ada sesuatu yang salah. Selain itu, dalam beberapa file mungkin ada kelas dengan nama yang sama, dan dengan satu permintaan salah satunya digunakan sebagai orangtua, dan dengan yang lain, kelas lain dari file lain digunakan (dengan nama yang sama, tetapi sama sekali berbeda). Ternyata ketika membuat kode yang akan dieksekusi pada beberapa permintaan, Anda tidak dapat merujuk ke kelas atau metode, karena mereka dibuat ulang setiap kali (masa hidup kode melebihi masa hidup kelas).

Preloading memungkinkan Anda untuk mengikat kelas pada awalnya dan, karenanya, menghasilkan kode lebih optimal. Minimal, untuk kerangka kerja yang akan dimuat menggunakan preloading.

Teknologi ini membantu tidak hanya untuk pengikatan kelas. Hal serupa diterapkan di Jawa sebagai Berbagi Data Kelas. Di sana, teknologi ini terutama ditujukan untuk mempercepat peluncuran aplikasi dan mengurangi jumlah total memori yang dikonsumsi. Plus yang sama diperoleh dalam PHP, karena sekarang pengikatan kelas tidak dilakukan dalam runtime, tetapi dilakukan hanya sekali. Selain itu, kelas terkait sekarang disimpan bukan di ruang alamat setiap proses, tetapi di memori bersama, dan oleh karena itu total konsumsi memori turun.

Menggunakan preloading juga membantu optimasi global semua skrip PHP, sepenuhnya menghapus overhead OPcache dan memungkinkan Anda untuk menghasilkan kode JIT yang lebih efisien.

Namun ada juga kekurangannya. Script yang dimuat saat startup tidak dapat diganti tanpa me-restart PHP. Jika kami mengunduh sesuatu dan membuatnya permanen, maka kami tidak dapat lagi menurunkannya. Oleh karena itu, teknologi ini dapat digunakan dengan kerangka kerja yang stabil, tetapi jika Anda menggunakan aplikasi beberapa kali sehari, kemungkinan besar itu tidak akan bekerja untuk Anda.

Teknologi ini dianggap transparan, yaitu memungkinkan memuat aplikasi yang ada (atau bagiannya) tanpa perubahan. Tetapi setelah implementasi, ternyata ini tidak sepenuhnya benar.Tidak semua aplikasi berfungsi seperti yang dimaksudkan jika mereka dimuat menggunakan preload . Misalnya, jika kode dipanggil dalam aplikasi berdasarkan hasil pemeriksaan function_exists atau class_exists , dan fungsi menjadi konstan, masing-masing, function_exists selalu mengembalikan true , dan kode yang sebelumnya dipanggil seharusnya dipanggil.

Secara teknis, preloading diaktifkan dengan bantuan hanya satu konfigurasi directive opcache.preload, ke input yang Anda berikan file skrip - file PHP biasa yang akan diluncurkan pada tahap startup aplikasi (tidak hanya dimuat, tetapi dieksekusi).

 <?php function _preload(string $preload, string $pattern = "/\.php$/") { if (is_file($path) && preg_match($pattern, $path)) { opcache_compile_file($path) or die("Preloading failed"); } else if (is_dir($path)) { if ($dh = opendir($path)) { while (($file = readdir($dh)) !== false) { if ($file !== "." && $file !== "..") { _preload($path . "/" . $file, $pattern); } } closedir($dh); } } } _preload("/usr/local/lib/ZendFramework"); 

Ini adalah salah satu skenario yang mungkin yang secara rekursif membaca semua file di beberapa direktori (dalam hal ini, ZendFramework). Anda dapat menerapkan sepenuhnya skrip apa pun dalam PHP: baca dengan daftar, tambahkan pengecualian, atau bahkan silang dengan komposer sehingga dapat podsoval file yang diperlukan untuk preloading. Ini semua masalah teknologi, dan yang lebih menarik bukanlah cara mengirim, tetapi apa yang harus dikirim.

Apa yang dimuat di preloading


Saya mencoba teknologi ini di WordPress. Jika Anda hanya mengunggah semua file * .php, maka WordPress akan berhenti bekerja karena fitur yang disebutkan sebelumnya: ia memiliki pemeriksaan function_exists, yang selalu menjadi kenyataan. Oleh karena itu, saya harus sedikit memodifikasi skrip dari contoh sebelumnya (tambahkan pengecualian), dan kemudian, tanpa perubahan di WordPress itu sendiri, itu berhasil.

Kecepatan [req / seq]Memori [MB]Jumlah skripJumlah fungsiJumlah kelas
Tidak ada3780000
Semua (hampir *)3957.52541770148
Hanya skrip yang digunakan3964,584153251

Akibatnya, karena preloading kami mendapat akselerasi ~ 5% , yang sudah tidak buruk.

Saya mengunduh hampir semua file, tetapi setengahnya tidak digunakan. Anda dapat melakukan yang lebih baik - menggerakkan aplikasi, melihat file mana yang telah diunduh. Anda dapat melakukan ini menggunakan fungsi opcache_get_status() , yang akan mengembalikan semua file yang di-cache OPcache dan membuat daftar untuk mereka untuk preloading. Dengan demikian, Anda dapat menghemat 3 MB dan mendapatkan sedikit lebih banyak akselerasi. Faktanya adalah semakin banyak memori yang dibutuhkan, semakin banyak cache prosesor menjadi kotor, dan semakin tidak efisien. Semakin sedikit memori yang digunakan, semakin tinggi kecepatannya.

FFI - Antarmuka Fungsi Asing


Teknologi lain yang terkait dengan JIT yang dikembangkan untuk PHP adalah FFI (Foreign Function Interface) atau, dalam bahasa Rusia, kemampuan untuk memanggil fungsi yang ditulis dalam bahasa pemrograman terkompilasi lain tanpa kompilasi. Implementasi teknologi semacam itu di Python membuat bos saya terkesan (Zeev Surazki), dan saya sangat terkesan ketika saya mulai menyesuaikannya dengan PHP.

Sudah ada beberapa upaya di PHP untuk membuat ekstensi untuk FFI, tetapi mereka semua menggunakan bahasa mereka sendiri atau API untuk menggambarkan antarmuka. Saya melihat ide di LuaJIT, di mana bahasa C (subset) digunakan untuk menggambarkan antarmuka, dan hasilnya adalah mainan yang sangat keren. Sekarang, ketika saya perlu memeriksa bagaimana sesuatu bekerja di C, saya menulisnya di PHP - itu terjadi, tepat di baris perintah.

FFI memungkinkan Anda untuk bekerja dengan struktur data yang didefinisikan dalam C dan dapat diintegrasikan dengan JIT untuk menghasilkan kode yang lebih efisien. Implementasi berbasis libffi sudah termasuk dalam PHP 7.4.

Tapi:

  • Ini adalah 1000 cara baru untuk menembak diri sendiri di kaki.
  • Membutuhkan pengetahuan C dan terkadang manajemen memori manual.
  • Tidak mendukung C-preprocessor (#include, #define, ...) dan C ++.
  • Performa tanpa JIT cukup rendah.

Meskipun, mungkin untuk beberapa itu akan nyaman, karena kompiler tidak diperlukan. Bahkan di bawah Windows, ini akan berfungsi tanpa Visual-C dari PHP.

Saya akan menunjukkan cara menggunakan FFI untuk mengimplementasikan aplikasi GUI nyata untuk Linux.

Jangan khawatir dengan kode C, saya sendiri menulis GUI di C 20 tahun yang lalu, tetapi saya menemukan contoh ini di Internet.

 #include <gtk/gtk.h> static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "Hello from C"); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); gtk_widget_show_all(window); } int main() { int status; GtkApplication *app; app = gtk_application_new("org.gtk.example", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), 0, NULL); g_object_unref(app); return status; } 

Program membuat aplikasi, tergantung pada acara aktifkan panggilan balik, meluncurkan aplikasi. Dalam panggilan balik, buat jendela, tetapkan ukuran judul untuk itu dan perlihatkan.

Dan sekarang, hal yang sama ditulis ulang dalam PHP:

 <?php $ffi = FFI::cdef(" โ€ฆ // #include <gtk/gtk.h> ", "libgtk-3.so.0"); function activate($app, $user_data) { global $ffi; $window = $ffi->gtk_application_window_new($app); $ffi->gtk_window_set_title($window, "Hello from PHP"); $ffi->gtk_window_set_default_size($window, 200, 200); $ffi->gtk_widget_show_all($window); } $app = $ffi->gtk_application_new("org.gtk.example", 0); $ffi->g_signal_connect_data($app, "activate", "activate", NULL, NULL, 0); $ffi->g_application_run($app, 0, NULL); $ffi->g_object_unref($app); 

Di sini, objek FFI dibuat terlebih dahulu. Deskripsi antarmuka dikirim kepadanya sebagai input - intinya, file-h - dan perpustakaan yang ingin Anda unduh. Setelah itu, semua fungsi yang dijelaskan dalam antarmuka menjadi tersedia sebagai metode objek ffi, dan semua parameter yang ditransfer secara otomatis dan benar-benar transparan diterjemahkan ke dalam representasi mesin yang diperlukan.

Dapat dilihat bahwa semuanya persis sama seperti pada contoh sebelumnya. Satu-satunya perbedaan adalah bahwa dalam C kami mengirim panggilan balik sebagai alamat, dan dalam PHP, koneksi terjadi oleh nama yang diberikan oleh string.

Sekarang mari kita lihat seperti apa antarmuka itu. Di bagian pertama, kami menentukan tipe dan fungsi di C, dan di baris terakhir kami memuat pustaka bersama:

 <?php $ffi = FFI::cdef(" typedef struct _GtkApplication GtkApplication; typedef struct _GtkWidget GtkWidget; typedef void (*GCallback)(void*,void*); int g_application_run (GtkApplication *app, int argc, char **argv); unsigned long * g_signal_connect_data (void *ptr, const char *signal, GCallback handler, void *data, GCallback *destroy, int flags); void g_object_unref (void *ptr); GtkApplication * gtk_application_new (const char *app_id, int flags); GtkWidget * gtk_application_window_new (GtkApplication *app); void gtk_window_set_title (GtkWidget *win, const char *title); void gtk_window_set_default_size (GtkWidget *win, int width, int height); void gtk_widget_show_all (GtkWidget *win); ", "libgtk-3.so.0"); ... 

Dalam hal ini, definisi-C ini disalin dari file-h dari perpustakaan GTK, hampir tidak berubah.

Agar tidak mengganggu C dan PHP dalam file yang sama, Anda dapat meletakkan seluruh kode-C ke dalam file yang terpisah, misalnya, dengan nama gtk-ffi.h dan menambahkan ke awal beberapa define'ov khusus yang menentukan nama antarmuka dan pustaka untuk memuat:

 #define FFI_SCOPE "GTK" #define FFI_LIB "libgtk-3.so.0" 

Jadi, kami memilih seluruh deskripsi antarmuka C dalam satu file. Gtk-ffi.h ini hampir nyata, tetapi sayangnya, kami belum mengimplementasikan preprocessor C, yang berarti makro dan menyertakan tidak akan berfungsi.

Sekarang mari kita muat antarmuka ini dalam PHP:

 <?php final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::load(__DIR__ . "/gtk_ffi.h"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

Karena FFI adalah teknologi yang agak berbahaya, kami tidak ingin memberikannya kepada siapa pun. Mari kita setidaknya menyembunyikan objek FFI, yaitu menjadikannya pribadi di dalam kelas. Dan kita akan membuat objek FFI tidak menggunakan FFI::cdef , tetapi menggunakan FFI::load , yang hanya membaca file-h kita dari contoh sebelumnya.

Sisa kode tidak banyak berubah, hanya sebagai pengendali acara kami mulai menggunakan fungsi yang tidak disebutkan namanya dan melewati judul menggunakan pengikatan leksikal. Yaitu, kami menggunakan C dan kekuatan PHP, yang tidak tersedia dalam C.

Perpustakaan yang dibuat dengan cara ini sudah bisa digunakan dalam aplikasi Anda. Tapi itu bagus jika hanya bekerja pada baris perintah , dan jika Anda memasukkannya ke dalam server web, file gtk_ffi.h akan dibaca pada setiap permintaan, perpustakaan akan dibuat dan dimuat, mengikat akan dilakukan ... Dan semua pekerjaan berulang ini akan memuat server Anda.

Untuk menghindari hal ini dan, pada kenyataannya, mengizinkan penulisan ekstensi PHP dalam PHP itu sendiri, kami memutuskan untuk melewati FFI dengan preloading.

FFI + preloading


Kode tidak banyak berubah, hanya sekarang kita memberikan h-file ke preloading, dan kita mengeksekusi FFI::load langsung pada saat preloading, dan bukan ketika kita membuat objek. Yaitu, memuat perpustakaan, semua parsing dan binding dilakukan sekali (pada startup server), dan dengan bantuan FFI::scope("GTK") kami mendapatkan akses ke antarmuka yang dimuat sebelumnya dengan nama dalam skrip kami.

 <?php FFI::load(__DIR__ . "/gtk_ffi.h"); final class GTK { static private $ffi = null; public static function create_window($title) { if (is_null(self::$ffi)) self::$ffi = FFI::scope("GTK"); $app = self::$ffi->gtk_application_new("org.gtk.example", 0); self::$ffi->g_signal_connect_data($app, "activate", function($app, $data) use ($title) { $window = self::$ffi->gtk_application_window_new($app); self::$ffi->gtk_window_set_title($window, $title); self::$ffi->gtk_window_set_default_size($window, 200, 200); self::$ffi->gtk_widget_show_all($window); }, NULL, NULL, 0); self::$ffi->g_application_run($app, 0, NULL); self::$ffi->g_object_unref($app); } } 

Dalam perwujudan ini, FFI dapat digunakan dari server web. Tentu saja, ini bukan untuk GUI, tetapi dengan cara ini Anda dapat menulis, misalnya, mengikat ke database.

Ekstensi yang dibuat dengan cara ini dapat digunakan langsung dari baris perintah:
 $ php -d opcache.preload=gtk.php -r 'GTK::create_window(" !");' 

Kelebihan lain dari perkawinan silang dan preloading FFI adalah kemampuan untuk melarang penggunaan FFI untuk semua skrip tingkat pengguna. Anda dapat menentukan ffi.enable = preload, yang berarti bahwa kami mempercayai file yang dimuat sebelumnya, tetapi memanggil FFI dari skrip PHP biasa dilarang.

Bekerja dengan struktur data C


Fitur lain yang menarik dari FFI adalah dapat bekerja dengan struktur data asli. Anda dapat kapan saja membuat di memori setiap struktur data yang dijelaskan dalam C.

 <?php $points = FFI::new("struct {int x,y;} [100]"); for ($x = 0; $x < count($points); $x++) { $points[$x]->x = $x; $points[$x]->y = $x * $x; } var_dump($points[25]->y); // 625 var_dump(FFI::sizeof($points)); // 800  foreach ($points as &$p) { $p->x += 10; } var_dump($points[25]->x); // 35 

100 ( FFI::new != new FFI), integer. , C. PHP, . count, / foreach . 800 , PHP PHP' , 10 .

FFI:

  • PHP.
  • PHP.

Python/CFFI : (Cario, JpegTran), (ffmpeg), (LibreOfficeKit), (SDL) (TensorFlow).

, FFI .

- PHP. , , callback' , . FFI. , . FFI c JIT, , LuaJIT, . , , .

 for ($k=0; $k<1000; $k++) { for ($i=$n-1; $i>=0; $i--) { $Y[$i] += $X[$i]; } } 

FFI .
Native ArraysFFI Arrays
PyPy0,0100,081
Python0,2120,343
LuaJIt -joff0,0370,412
LuaJit -jon0,0030,002
Php0,0400,093
PHP + JIT0,0160,087

: Zeev Surasky (Zend), Andi Gutmans (ex-Zend, Amazon), Xinchen Hui (ex-Weibo, ex-Zend, Lianjia), Nikita Popov (JetBrains), Anatol Belsky (Microsoft), Anthony Ferrara (ex-Google, Lingo Live), Joe Watkins, Mohammad Reza Haghighat (Intel) Intel, Andy Wingo (JS hacker, Igalia), Mike Pall ( LuaJIT).

, , .

PHP Russia 2020 ! telegram- , 2019 youtube- , , โ€” .

Source: https://habr.com/ru/post/id481092/


All Articles