Ekstensi PHP opcache mengimplementasikan berbagai fungsi untuk mempercepat PHP secara transparan. Seperti namanya, asal dan tujuan utamanya adalah caching opcode, tetapi saat ini juga berisi pengoptimal dan kompiler just-in-time. Namun, posting blog ini hanya akan fokus pada aspek caching opcode
Opcache memiliki tiga lapis cache. Cache memori bersama yang asli, cache file diperkenalkan di PHP 7, dan fungsionalitas preloading ditambahkan di PHP 7. 4. Kami akan membahas semua ini secara bergantian
Sementara opcache secara nominal merupakan ekstensi independen, fungsinya sangat bergantung pada detail implementasi mesin, dan modifikasi pada mesin sering kali memerlukan perubahan pada opcache juga. Dengan demikian, cara kerja opcache berbeda secara signifikan antara versi PHP. Artikel ini menjelaskan status pada PHP 8. 1 dan menyoroti beberapa perubahan dalam versi ini
Berbagi memori
Tujuan utama opcache adalah untuk meng-cache artefak kompilasi di memori bersama, untuk menghindari keharusan mengkompilasi ulang skrip PHP pada setiap eksekusi
Pada sistem mirip Unix, satu segmen memori bersama ukuran tetap (SHM) dialokasikan saat startup. Untuk menangani permintaan, PHP kemudian akan melakukan proses tambahan atau menelurkan utas tambahan. Proses/utas ini akan melihat segmen SHM di alamat yang sama
Karena Windows tidak mendukung forking, biasanya malah menelurkan proses PHP yang sepenuhnya terpisah, yang tidak memiliki ruang alamat bersama. Ini adalah masalah besar untuk opcache, karena mengharuskan segmen SHM dipetakan pada alamat yang sama di setiap proses. Jika tidak, pointer ke SHM tidak akan valid di seluruh proses
Untuk membuat ini berfungsi, opcache menyimpan alamat dasar SHM, dan mencoba memetakan segmen di alamat yang sama di proses lain. Jika ini gagal, opcache kembali menggunakan file cache. Namun, meski berhasil, ada batasannya. Meskipun ini menjamin alamat yang sama untuk segmen SHM, alamat fungsi/kelas internal mungkin berbeda antar proses karena ASLR. Ini berarti bahwa pada Windows, artefak yang di-cache tidak mungkin bergantung pada fungsi/kelas internal, dll
Windows adalah satu-satunya platform di mana dua proses PHP yang tidak terkait dapat berbagi SHM opcache yang sama. Misalnya, dua pemanggilan CLI bersamaan dapat berbagi cache yang sama, yang tidak mungkin dilakukan pada sistem operasi lain. Pengaturan opcache.cache_id ada untuk memaksa cache yang berbeda dalam kasus ini
Karena mempertahankan perilaku terpisah untuk Windows adalah sesuatu yang menyusahkan, opcache dapat menghentikan dukungan untuk pemasangan kembali dari proses yang tidak terkait di masa mendatang, yang berarti bahwa pada Windows, penggunaan SAPI berbasis utas daripada berbasis proses akan diperlukan.
Mengunci dan kekekalan
Saat memori bersama digunakan, selalu penting untuk mempertimbangkan model akses Anda. Karena kami tidak ingin melakukan operasi penguncian berbutir halus atau penghitungan referensi atom saat runtime, model memori opcache akhirnya menjadi sangat sederhana. Memori bersama tidak dapat diubah
Opcache pada dasarnya hanya memiliki dua kunci. Salah satunya adalah kunci tulis, yang hanya dapat dipegang oleh satu proses yang diizinkan untuk memodifikasi SHM. Saat kunci tulis ditahan, proses lain masih diizinkan untuk membaca SHM. Dengan demikian, memegang kunci tulis umumnya hanya memungkinkan Anda untuk mengalokasikan memori baru di segmen SHM dan menulis padanya, tetapi tidak mengubah memori bersama yang sudah dialokasikan dan berpotensi digunakan (dengan beberapa pengecualian)
Opsi opcache.protect_memory_ dapat digunakan untuk melindungi seluruh segmen SHM setiap kali kunci tulis tidak ditahan, yang berguna untuk mendeteksi pelanggaran invarian kekekalan (tetapi tidak boleh diaktifkan dalam produksi karena alasan kinerja)
Kunci lainnya adalah kunci baca yang diperoleh saat permintaan menggunakan SHM untuk pertama kalinya. Itu tidak melacak apa yang sedang digunakan dan apakah itu berhenti digunakan. Satu-satunya tujuan adalah untuk mencatat bahwa cache sedang digunakan dalam permintaan ini
Tujuan dari penguncian ini adalah untuk memfasilitasi restart opcache. Karena kami tidak melacak bagian mana dari cache yang digunakan dengan sangat halus, tidak mungkin menghapus apa pun dari cache opcode. Saat cache sudah penuh, sebagai gantinya dijadwalkan mulai ulang
Jika mulai ulang dijadwalkan, maka permintaan yang baru dimulai tidak akan menggunakan cache SHM (tetapi mungkin kembali ke cache file). Saat jumlah pengguna turun menjadi nol, seluruh cache dihapus dan kita dapat memulai dari awal. Jika jumlah pengguna tidak turun ke nol dalam opcache.force_restart_timeout, maka opcache akan mematikan pengguna yang tersisa
Petunjuk peta
Beberapa struktur yang disimpan dalam cache SHM perlu (atau setidaknya ingin) merujuk data per permintaan. Misalnya, meskipun definisi fungsi umumnya tidak dapat diubah, definisi tersebut mungkin berisi variabel statis, yang akan berbeda untuk setiap permintaan. Demikian pula, fungsi menggunakan run-time cache untuk meng-cache resolusi simbol khusus permintaan
Karena kami tidak dapat menyimpan informasi per-permintaan dalam cache memori bersama yang tidak dapat diubah, kami menggunakan tipuan "penunjuk peta" sebagai gantinya. Daripada menyimpan pointer ke variabel statis, kami malah menyimpan referensi ke mana variabel statis akan disimpan
Dalam implementasi saat ini, penunjuk peta mengambil salah satu dari dua bentuk. Entah itu adalah penunjuk biasa ke penyimpanan penunjuk yang sebenarnya, yang merupakan representasi yang digunakan ketika struktur tidak di-cache di SHM. Pointer tipuan biasanya dialokasikan arena
Alternatifnya, penunjuk peta hanya menyimpan offset dari alamat dasar, di mana alamat dasar akan berbeda untuk setiap permintaan. Ini adalah representasi yang digunakan untuk struktur yang tidak dapat diubah dalam memori bersama. Kami melacak seberapa besar area penunjuk peta yang digunakan harus dan membidiknya pada setiap permintaan
For mutable memory: map_ptr & 1 == 0 map pointer ----> indirection pointer -----> static variables (arena allocated) For immutable memory: map_ptr & 1 == 1 map base pointer: slot 0 slot 1 + map offset: slot 2 -----> static variables slot 3
Meskipun sudah jelas mengapa kita memerlukan tipuan dalam kasus kedua (pisahkan area penunjuk peta untuk setiap permintaan), orang mungkin bertanya-tanya apa tujuan penunjuk tipuan dalam kasus pertama. Karena memori bisa berubah, kita bisa menyimpan penunjuk variabel statis secara langsung. Ini memang hanya artefak sejarah, dan tipuan yang tidak perlu kemungkinan besar akan hilang di PHP 8. 2
String yang diinternir
Pada titik ini, mari kita kesampingkan sebentar untuk membahas string yang diinternir. String dalam PHP direpresentasikan sebagai struktur yang dihitung referensi yang menyimpan panjang string, isinya, dan hashnya. Meskipun string dapat dibagikan, mungkin juga ada beberapa string dengan konten yang sama, jika dibuat secara terpisah
String yang diinternir dideduplikasi. Hanya akan ada satu string yang diinternir dengan konten tertentu. Ini menghemat memori dan dapat membuat perbandingan lebih efisien, karena jalur cepat persamaan penunjuk lebih cenderung terpicu. String yang diinternir dalam PHP juga tidak dapat diubah karena tidak dihitung referensi
Tanpa opcache, PHP memisahkan string yang diinternir menjadi persisten dan per-permintaan. String yang diinternir terus-menerus dibuat selama startup, misalnya untuk nama kelas/fungsi internal. String per-permintaan dibuat untuk simbol dan literal dalam skrip PHP (jika belum ada string tetap yang diinternir untuknya) dan dibuang di akhir permintaan
Ketika opcache diaktifkan, string yang diinternir disimpan di SHM, sehingga duplikasinya dihapus di seluruh proses dan dapat dirujuk oleh struktur yang di-cache di SHM. Saat startup, opcache akan menyalin string yang diinternir ke dalam SHM berdasarkan upaya terbaik (mungkin tidak mengetahui tentang semua pointer yang disimpan di suatu tempat), tetapi ini tidak penting untuk kebenaran
Selain itu, pembuatan string yang diinternir selama permintaan dinonaktifkan. Sebagai gantinya, string normal yang tidak diasingkan akan dibuat. Hanya ketika skrip yang dikompilasi di-cache (dan kunci tulis SHM diperoleh) string dapat diubah menjadi string yang diinternir SHM
Cache entri kelas
Skrip PHP berisi banyak referensi ke kelas dalam bentuk string, mis. g. new Foo atau tipe Foo $param. Karena identitas sebenarnya dari Foo mungkin berbeda di antara permintaan, tidak mungkin untuk mengompilasinya menjadi referensi kelas langsung
Mengambil entri kelas dari nama kelas relatif mahal untuk seberapa umum itu. Kita perlu mengecilkan string dan mencarinya di tabel hash kelas. Untuk referensi seperti new Foo pencarian ini di-cache dalam cache run time fungsi. Namun, tidak selalu memungkinkan untuk menggunakan cache run time. Misalnya, pemeriksaan tipe properti tidak dapat menggunakan cache run time dan sebelum PHP 8. 1 digunakan untuk mengganti nama string dengan entri kelas langsung di dalam tipe, yang berarti tipe tersebut tidak dapat hidup di SHM
PHP 8. 1 memperkenalkan cache entri kelas, yang menggabungkan string yang diinternir dengan penunjuk peta. Untuk string yang diinternir yang digunakan pada posisi tertentu (deklarasi kelas dan nama tipe), slot penunjuk peta dialokasikan, yang menyimpan entri kelas yang diselesaikan untuk nama ini. Untuk menghindari bertambahnya ukuran string, ini menggunakan trik
Biasanya, string yang diinternir selalu memiliki jumlah referensi 2. Namun, jumlah referensi sebenarnya tidak masalah, hanya perlu lebih besar dari 1 untuk memastikan string terduplikasi saat modifikasi. String dengan refcount 1 dapat dimodifikasi di tempat. Dengan demikian, kita dapat menggunakan bidang refcount untuk menyimpan offset penunjuk peta untuk digunakan sebagai cache entri kelas
Ini memang datang dengan beberapa batasan, karena terikat pada mekanisme string yang diinternir. Misalnya, jika opcache diaktifkan tetapi skrip tidak di-cache, maka string yang diinternir tidak akan digunakan dan akibatnya cache entri kelas tidak akan tersedia
Salah satu hal yang menyenangkan tentang cache entri kelas adalah cukup umum dan tidak terikat pada konstruksi bahasa tertentu (seperti cache run-time). Jika Anda menulis new ReflectionClass(Foo::class), pencarian kelas dapat di-cache, meskipun terjadi secara dinamis
Bertahan
Kegigihan sebenarnya dari skrip ke dalam memori bersama relatif mudah. Skrip pertama kali dikompilasi seperti biasa, terlepas dari beberapa opsi untuk memastikan tidak ada ketergantungan lintas file yang digunakan selama kompilasi. Hasil kompilasi dipindahkan dari tabel fungsi/kelas global ke dalam struktur skrip persisten mandiri
Kemudian ukuran segmen memori bersama yang diperlukan dihitung. Langkah ini harus persis mencerminkan logika dari langkah bertahan yang sebenarnya, tetapi (kebanyakan) tidak mengubah skrip. Jika alokasi memori bersama gagal, kita masih bisa mem-bypass opcache dan menjalankannya seperti biasa. Satu-satunya modifikasi yang dilakukan langkah "persist calc" adalah mengubah string menjadi string yang diinternir SHM jika memungkinkan, karena string yang diinternir disimpan dalam segmen ukuran tetap yang terpisah dari skrip yang bertahan. String yang berhasil diinternir tidak diperhitungkan dalam ukuran skrip
Terakhir, langkah bertahan menyalin skrip ke memori bersama dan membebaskan skrip asli. Untuk melakukannya, ini melacak tabel xlat, yang memetakan pointer asli ke pointer baru di memori bersama. Ini memungkinkan penyelesaian penggunaan pointer yang sama berulang kali
Tembolok warisan
Kelas secara internal datang dalam dua bentuk. Kelas yang tidak tertaut mewakili deklarasi kelas seperti yang Anda tulis dalam kode. Ini berisi metode yang dideklarasikan dalam kelas itu dan ketergantungan referensi (kelas induk, antarmuka, sifat) sebagai string. Kelas tertaut mewakili deklarasi kelas yang telah berhasil menyelesaikan pewarisan. Ini berisi metode/properti/dll yang diwariskan dan ketergantungan referensi sebagai entri kelas yang diselesaikan
Saat melihat satu skrip, kelas biasanya ada dalam bentuk yang tidak ditautkan (kecuali jika tidak memiliki ketergantungan). Menautkan kelas membutuhkan melihat kelas di file lain. Namun, deklarasi kelas yang digunakan mungkin berbeda dari satu permintaan ke permintaan berikutnya
Sebelum PHP 8. 1, ini berarti bahwa hanya template kelas yang tidak ditautkan yang di-cache, dan pewarisan masih harus dilakukan pada setiap permintaan. Karena pewarisan adalah proses yang cukup mahal, ini memiliki dampak kinerja yang tidak sepele. PHP 8. 1 mengatasinya dengan memperkenalkan cache warisan
Cache warisan menyimpan hasil warisan tertaut untuk sekumpulan dependensi tertentu. Saat pewarisan diminta saat run-time, dependensi nama kelas diselesaikan menjadi entri kelas dan jika entri cache untuk kumpulan dependensi ini sudah ada, itu akan digunakan. Meskipun dependensi dapat berbeda di antara permintaan, dalam praktiknya biasanya akan sama, jadi pewarisan hanya perlu dilakukan sekali
Jika tidak ada entri cache, kelas yang tidak ditautkan disalin dari SHM ke dalam memori per-proses yang dapat berubah dan proses pewarisan dilakukan di atasnya (di tempat). Hasilnya disimpan ke dalam cache warisan menggunakan proses persistensi normal, bersama dengan dependensi yang entri cache ini valid
Pramuat
Preloading adalah solusi yang lebih radikal untuk masalah pewarisan. Apa pun yang dimuat oleh skrip preload akan bertahan di seluruh permintaan. Dengan demikian, aman untuk menggunakan ketergantungan skrip silang dalam kasus ini. Kerugiannya adalah preload state tidak dapat diubah tanpa me-restart PHP
Beberapa manfaat preloading kemungkinan telah usang oleh cache warisan di PHP 8. 1, meskipun preloading masih memiliki beberapa kelebihan. Kelas tersedia dalam bentuk yang sepenuhnya diwariskan pada awal permintaan. Satu-satunya biaya pra-pemuatan per permintaan adalah membersihkan area penunjuk peta. Penggunaan opcache normal masih memerlukan autoloading, mencari skrip persisten, mendaftarkan entri dalam tabel hash global, mencari dan memeriksa dependensi untuk cache warisan, dll.
Preloading dapat beroperasi dalam dua mode. Ketika kelas dimuat dengan mudah menggunakan require_, pewarisan akan terjadi seperti biasanya dan pemuatan awal dapat mendukung kelas dengan skenario pewarisan kompleks yang sewenang-wenang (termasuk siklus varians). Ini juga memudahkan untuk memastikan bahwa semua dependensi yang diperlukan disediakan oleh pemuat otomatis
Sebagai alternatif, dimungkinkan untuk melakukan pramuat file menggunakan opcache.cache_id0. Dalam hal ini opcache akan mencoba memuat kelas terlebih dahulu jika semua dependensinya juga tersedia. Jika tidak, itu akan mengeluarkan peringatan dan menyimpan skrip dengan cara lama. Sebelum PHP 8. 1 persyaratan "semua dependensi" agak bermasalah
Di versi PHP sebelumnya, kelas yang tidak ditautkan dipertahankan dalam dua bagian. Satu yang benar-benar tidak dapat diubah, dan satu lagi yang harus disalin ke dalam memori per permintaan, karena dapat dimodifikasi saat run-time. Ini termasuk tipe properti serta penginisialisasi konstanta/properti. Jika ini tidak dapat diselesaikan sepenuhnya selama pemuatan awal, kelas tidak dapat dimuat sebelumnya, karena kami tidak dapat melakukan salinan per permintaan dalam kasus tersebut. Di PHP8. 1 semua bagian yang dapat dimodifikasi run-time yang tersisa dialihkan ke penunjuk peta, sehingga melonggarkan kendala pada apa yang dianggap sebagai "ketergantungan". Sekarang ini hanya mencakup orang tua/antarmuka/sifat, serta tipe yang diperlukan untuk melakukan pemeriksaan varians
Pemeriksaan varians adalah masalah lain. Diperlukan atau tidaknya tipe argumen/pengembalian untuk melakukan pemeriksaan varian sangat sulit untuk ditentukan sebelumnya. Ini tergantung pada apakah suatu metode sebenarnya merupakan override (yang tidak jelas dengan adanya sifat) dan apakah hubungan subtipe dapat ditentukan tanpa memuat kelas (mis. g. jika jenis dalam metode induk dan anak persis sama). Versi PHP sebelumnya menyelesaikan ini secara heuristik, membutuhkan lebih banyak ketergantungan daripada yang diperlukan. PHP 8. 1 sebagai gantinya hanya akan mencoba pewarisan pada salinan kelas dan membuangnya jika gagal
Ini berarti bahwa preloading berbasis opcache.cache_id_0 harus lebih dapat diprediksi di PHP 8. 1
Tembolok file
Cache file yang diperkenalkan di PHP 7 dapat digunakan secara mandiri (opcache.cache_id2) atau bersama dengan cache SHM sebagai cache tingkat kedua. Dalam kasus terakhir, itu akan digunakan pada start dingin, atau ketika cache SHM tidak tersedia selama restart opcache. Di Windows, fallback cache file diaktifkan secara default, untuk memastikan bahwa setidaknya beberapa caching tersedia jika reattachment SHM gagal
Serialisasi cache file dimulai dari representasi skrip yang bertahan, baik di SHM (tingkat kedua) atau di wilayah memori sementara (mandiri), tetapi dibuat menggunakan mekanisme persistensi yang biasa. Serialisasi sebenarnya kemudian menggantikan semua pointer dengan offset ke dalam wilayah memori ("pointer unswizzling"). Ini memungkinkan unserialisasi yang efisien dengan menambahkan pointer basis baru ke semua pointer
Komplikasi utama dalam model ini adalah string yang diinternir, karena ini adalah satu-satunya petunjuk yang tidak mengarah ke wilayah memori yang bertahan. String yang diinternir yang direferensikan malah diserialisasikan ke dalam wilayah memori yang terpisah. Pada unserialization, upaya dilakukan untuk mengonversinya kembali ke string yang diinternir SHM
Unserialization bekerja dengan menyalin konten file (termasuk skrip serial dan area string yang diinternir) ke dalam buffer. Dalam mode mandiri, buffer ini bersifat non-sementara dan unserialization (pointer swizzling) terjadi langsung di buffer ini
Dalam mode tingkat kedua, buffer ini biasanya bersifat sementara. Alih-alih, alokasi SHM dibuat, di mana skrip berseri disalin dan di mana skrip tidak diserialisasi. Dalam hal ini semua string yang diinternir juga perlu diubah menjadi string yang diinternir SHM. Buffer sementara kemudian dapat dibuang. Namun, jika tidak semua string yang diinternir dapat disisipkan karena string buffer overflow yang diinternir, maka segmen SHM ditinggalkan dan unserialization per permintaan seperti dalam kasus mandiri dilakukan