VM Bytecode
Jalur eksekusi default adalah tree-walker — kunjungi setiap node AST, lalu kembalikan nilai. Untuk fungsi yang sering dipanggil hingga jutaan kali, overhead dispatch per node menjadi hambatan. Phasis menambahkan compiler bytecode-ke-closure PHP spekulatif di atasnya untuk mengurangi overhead tersebut.
Kapan VM mulai bekerja
Setiap JsFunction memiliki penghitung pemanggilan panas. Setelah melewati ambang batas (default 100), engine mencoba mengompilasi isi fungsi menjadi closure PHP melalui src/Bytecode/JsToPhp.php.
Kompilasi berhasil jika isi fungsi sesuai dengan sekumpulan bentuk yang dikenal:
- Penetapan variabel lokal dengan tipe yang dapat diprediksi (numerik, objek, fungsi, array, string).
- Operasi aritmetika pada angka.
- Pembacaan anggota pada variabel lokal bertipe objek DAN pada
this(this.v,obj.x). - Pemanggilan metode pada variabel lokal bertipe objek (
obj.method(args)), dengan receiver diteruskan sebagai$thisValue. - Pemanggilan konstruktor
new Ctor(args)— mempromosikan tipe variabel lokal LHS menjadiobjectdan menjaga dengan Bailout jika nilai kembalian bukan objek. - Loop
for/whiledengan bentuk kondisi sederhana. returndengan ekspresi yang dapat dihitung.if/elsedengan kondisi bertipe.
Kompilasi gagal (dan fungsi tetap menggunakan tree-walker) jika isi fungsi memuat:
- Pernyataan
with. try / catch / finally(ditangani oleh tree-walker agar semantik penyelesaian tetap benar).- Pemanggilan
eval(). - Mutasi
argumentslangsung. - Apa pun yang belum diketahui cara menurunkannya oleh compiler.
Jika kompilasi berhasil, engine mengalihkan jalur pemanggilan fungsi ke closure hasil kompilasi. Pemanggilan berikutnya menjalankan PHP native dengan satu overhead pemanggilan fungsi, bukan satu overhead untuk setiap node AST.
Batal kompilasi
Jalur cepat hasil kompilasi membuat asumsi (angka tetap berupa angka, properti tetap memiliki bentuk yang sama). Jika asumsi dilanggar saat runtime, closure akan memunculkan Bailout dan eksekusi dimulai ulang di tree-walker untuk pemanggilan tersebut.
Di sinilah ketepatan terhadap spesifikasi paling penting. Tree-walker harus dapat mengeksekusi ulang pemanggilan yang batal dari awal tanpa melihat efek samping apa pun dari jalur cepat yang gagal. Phasis menangani hal ini dengan dua cara:
- Awalan murni — compiler memindai efek samping sebelum spekulasi apa pun. Jika fungsi melakukan I/O (console.log, fungsi host) sebelum titik spekulasi, jalur cepat tidak menghilangkannya; jalur itu melakukan commit lalu melanjutkan.
- Penolakan saat kompilasi — untuk bentuk yang memerlukan pembatalan efek samping yang dapat diamati (
let r = fn()nonnumerik yangfn()memutasi state), compiler menolak melakukan kompilasi. Fungsi tetap menggunakan tree-walker.
Kasus yang mendasari: penetapan nonnumerik menjalankan fn() pada jalur cepat, memunculkan Bailout, lalu dijalankan ulang di tree-walker — sehingga fn() berjalan dua kali dengan dua rangkaian efek samping. Penolakan saat kompilasi mencegah fn() berjalan sama sekali di jalur cepat, sehingga tree-walker hanya melihat satu pemanggilan.
Dispatch VM (lapisan kedua)
Sebagian fungsi dikompilasi menjadi closure PHP; fungsi lainnya diteruskan ke interpreter bytecode yang lebih sederhana di src/Bytecode/VM.php. Bytecode berupa array opcode datar (LOAD_LOCAL, STORE_PROP, CALL_METHOD, …) yang dijalankan oleh dispatch switch.
VM digunakan untuk fungsi yang tidak layak dikompilasi menjadi closure PHP tetapi cukup sering dipanggil sehingga diuntungkan dengan melewati traversal AST. Pemanggilan metode pada objek bawaan (Array.prototype.map, String.prototype.split, …) melalui jalur cepat di VM yang melewati trampoline callFunction generik.
Call stack khusus
Op::CALL, Op::CALL_METHOD, dan Op::NEW_CALL menggunakan jalur pemanggilan inline yang menjaga seluruh call stack JS di dalam VM::execute — tanpa frame PHP baru untuk setiap pemanggilan JS. Saat masuk, state pemanggil (pc, aliran kode, stack operand, variabel lokal, env, this, flag mode ketat) didorong ke pool savedFrames per-VM yang berisi instans SavedFrame, lalu loop dispatch mengalihkan variabel lokalnya ke callee. Op::RET mengambil frame tersimpan dan memulihkannya. Exception diurai melintasi frame tersimpan sebelum kembali ke tree-walker.
Kelayakan dimemoisasi pada JsFunction: vmInlineCallCache untuk pemanggilan biasa (isi terkompilasi BC, tidak terikat ke bind(), bukan native), vmInlineConstructCache untuk ekspresi new (selain itu isConstructable, bukan turunan, tanpa inisialisasi field instans atau metode privat). Selebihnya tetap melalui trampoline kanonis callFunction / vmNewExpression.
Manfaat arsitektural lainnya: batas stack PHP tidak lagi membatasi kedalaman rekursi JS. Kini hanya ada batas CallStack::maxDepth — ditetapkan sebesar 4 096, jauh di atas semua kasus test262 yang wajar (sebagian besar memerlukan kurang dari 2 k frame) dan cukup ketat agar pengujian rekursi tak terbatas yang patologis gagal dengan cepat dalam batas waktu runner. Jalur closure yang ditranspilasi (JsToPhp) memiliki penjaga tersendiri untuk 8 frame 192 pada jalur fallback yang tidak di-inline.
Generator snapshot frame
Isi generator sederhana — tanpa yield*, tanpa yield di dalam try/catch/finally, tanpa await — dikompilasi menjadi bytecode. Compiler menghasilkan opcode Op::YIELD baru di lokasi isi fungsi ditangguhkan; Op::YIELD menangkap status dispatch (cf, pc+1, stack, sp, variabel lokal, env, thisValue, strict) ke dalam objek heap GeneratorSnapshot lalu mengembalikan sentinel YieldResult dari VM::execute. JsGenerator penggerak menyimpan snapshot dan mengirimkannya kembali ke VM::execute pada next() / throw() berikutnya, mengganti nilai resume di puncak stack operand.
Ini menggantikan jalur Fiber PHP untuk subset yang didukung. Driver berbasis Fiber (executeGeneratorBody) tetap digunakan untuk bentuk yang ditolak compiler — delegasi yield*, generator yang melakukan yield di dalam blok try, generator asinkron, fungsi asinkron dengan await. JsGenerator mendukung dua mode: setiap next() / throw() / return() memeriksa apakah instans memiliki snapshot atau Fiber lalu melakukan dispatch sesuai kondisi.
Tidak ada opsi yang dapat disetel untuk benchmark; kelayakan ditentukan saat kompilasi dan dimemoisasi pada JsFunction. Keuntungannya adalah satu pergantian konteks PHP lebih sedikit untuk setiap yield pada jalur snapshot; jalur Fiber untuk semua bentuk lainnya tidak berubah.
Cache inline
Op::LOAD_MEMBER memiliki cache inline per-PC yang dikunci berdasarkan identitas prototipe, digunakan untuk pola metode prototipe pada instans (c.sum(...), arr.push(...), setiap dispatch metode kelas). Kondisi cache hit: receiver tidak memiliki slot sendiri untuk nama tersebut, referensi prototipe yang disimpan identik dengan getPrototype() milik receiver, dan PropertyMap::$version prototipe masih sama dengan versi yang diambil.
Invalidasi cache mengikuti PropertyMap::$version. Mutasi struktural (set / delete) selalu menaikkan nilainya. Penimpaan nilai slot data hanya menaikkannya jika PropertyMap::$isUsedAsProto bernilai true — flag write-barrier yang diaktifkan saat pertama kali objek diamati sebagai [[Prototype]] milik objek lain. Objek instans yang tidak pernah berfungsi sebagai prototipe tidak menanggung biaya infrastruktur IC pada setiap obj.x = v.
Accessor getter tidak pernah di-cache (harus dijalankan kembali pada setiap pembacaan).
PropertyMap memiliki penghitung kedua, $mutationVersion, yang naik tanpa syarat pada setiap penulisan (tanpa write-barrier). Cache JSON.stringify menyematkan penghitung ini pada setiap objek/array bertingkat yang dikunjungi, sehingga mutasi yang hanya terjadi pada instans seperti o.a = 99 dengan benar membatalkan cache string meskipun $version milik IC seharusnya melewatkan kenaikan tersebut.
Cache JSON.stringify
JsObject memuat $jsonCacheString + $jsonCacheVersions (daftar pasangan [obj, mutationVersion] untuk setiap objek bertingkat yang dikunjungi selama proses trivial-stringify). Pada pemanggilan JSON.stringify(o) berikutnya tanpa replacer / tanpa spasi, jalur cepat trivial memverifikasi bahwa setiap versi yang disematkan tidak berubah dan mengembalikan string yang tersimpan di cache tanpa menelusuri ulang atau mengodekan ulang. Pola deep-clone yang sering digunakan seperti JSON.parse(JSON.stringify(o)) mendapatkan peningkatan kecepatan sekitar 2× pada bagian stringify.
Cache dihapus tanpa pemberitahuan saat terjadi miss (versi yang disematkan berbeda) dan diisi kembali pada penelusuran berikutnya. Objek dengan toJSON, getter, accessor, simbol, atau slot [[PrimitiveValue]] beralih ke serializer yang sesuai spesifikasi dan tidak pernah mengisi cache — sehingga jalur cache hanya melihat data yang seragam secara struktural.
Jalur cepat bawaan
Peningkatan performa terbesar berasal dari mengajari VM tentang built-in tertentu:
Atomics.load/store/compareExchangedengan penjadwalan kooperatifloadSpinHook/storeNotifyHook— memulihkan 4 pengujian loop spin Atomics.Date.prototype.getTimezoneOffsetdengan cache DST direct-hashmap — peningkatan kecepatan 45 % pada pengujian stres DST SpiderMonkey.String.fromCharCodedengan jalur cepat ASCII inline — menghemat ~1 juta dispatch per pemindaian tipikal.decodeURI/decodeURIComponentdengan jalur supercepat%XX%XX%XX%XXsepanjang 12 karakter yang menggunakan decoding bulkhex2bin.Object.keys/Object.values/Object.entriesdengan akses langsung ke peta properti.Array.prototype.map/filter/forEach/reducedengan iterasi native pada array PHP yang mendasarinya.
Setiap jalur cepat dikendalikan oleh "pemeriksaan bentuk" — jika penerima tidak cocok dengan bentuk yang diharapkan (e.g, tampilan TypedArray yang tidak didukung oleh PHP string biasa), VM beralih ke jalur lambat yang sesuai spesifikasi.
Hal yang tidak dilakukan
VM bytecode bukan JIT dalam pengertian V8. VM ini tidak menghasilkan kode mesin native untuk tiap fungsi dan tidak membuat spekulasi berdasarkan tipe yang diamati di luar pemeriksaan identitas prototipe monomorfik IC. Inlining pada batas fungsi didekati melalui callstack khusus (tanpa frame PHP tambahan per pemanggilan JS), bukan dengan menurunkan isi callee ke bytecode caller.
Transpiler JsToPhp adalah bagian engine yang paling mendekati JIT: transpiler ini menghasilkan kode sumber PHP untuk isi fungsi yang sering digunakan, lalu JIT tracing milik PHP 8.5 mengompilasinya menjadi kode native. JsToPhp berjalan melalui eval(), sehingga memerlukan biaya kompilasi satu kali dan JIT tracing harus menemukan fungsi tersebut seolah-olah merupakan kode pengguna. Melewati langkah eval() dengan menghasilkan opcode opcache secara langsung akan memperkecil kesenjangan — lihat Tolok Ukur § Peta Jalan.
Tolok ukur
bench/microbench.js menjalankan belasan benchmark kecil (loop-arith, loop-fib, fn-recurse, fn-deep-recurse, obj-create, obj-prop, proto-method, gen-iterate, arr-push, arr-map, str-concat, str-split-join, json-roundtrip, closure, destructure). Waktu dinding median:
php bench/run.php
Angka terkini di-commit ke BENCH.md setelah setiap proses workflow bench. Workflow matriks bench dan test262 sama-sama dijalankan secara manual.