Benchmark
Phasis è circa due ordini di grandezza più lento di V8 nelle operazioni JavaScript limitate dal dispatch. Questo limite è il costo intrinseco di un interprete ospitato da PHP rispetto a un compilatore JIT nativo. Per chi integra il motore, la domanda non è «come si confronta con V8», ma «è abbastanza veloce per ciò che devo fare?». Per la maggior parte dei carichi di lavoro di integrazione, la risposta è sì.
Quando il limite non conta
- Templating — il rendering SSR di una pagina tipica richiede molto meno di 10 ms.
- Regole di convalida — JS fornito dall'utente per la convalida dei moduli viene eseguito in microsecondi.
- Trasformazioni dei dati —
array.map(transform)su centinaia di elementi si completa in decine di millisecondi. - Runner sandbox — gli script brevi forniti dall'utente, con un limite host di 1 secondo, hanno ampio margine.
Quando il limite conta
- Cicli interni ad alto carico di calcolo — elaborazione di immagini, riduzioni di array di grandi dimensioni, simulazioni. Spostate i calcoli in una libreria PHP nativa ed esponetela come funzione host.
- Animazione in tempo reale — tutto ciò che deve raggiungere 60 fps. Questa JS dovrebbe risiedere nel client (browser o app nativa), non nel motore integrato.
- Reimplementazioni JS di algoritmi onerosi dal punto di vista computazionale — crittografia, compressione, analisi. PHP dispone di equivalenti nativi 1000× più veloci: usate quelli.
Il modello mentale corretto: Phasis è uno strato di collegamento per la logica in stile JS integrata in applicazioni in stile PHP. L'host PHP gestisce i calcoli; lo strato JS gestisce configurazione, diramazioni e piccole trasformazioni.
Cosa rende veloce il motore (rispetto a un tree-walker ingenuo)
- VM bytecode — la maggior parte del dispatch più frequente passa per lo switch degli opcode di
VM::execute, non per l'AST. - Stack di chiamate personalizzato — ogni chiamata JS mantiene lo stato dentro
VM::executee salva il frame del chiamante in un pool; non viene creato un nuovo frame PHP per ogni chiamata JS. Vedi Stack di chiamate personalizzato in VM bytecode. - Cache inline — una cache monomorfica per PC consente alle letture di metodi dei prototipi sulle istanze (il modello
c.method()) di saltare l'attraversamento della catena dei prototipi. - Generatori con snapshot del frame — i corpi semplici dei generatori sospendono l'esecuzione acquisendo lo stato del dispatch in un oggetto heap invece di passare attraverso un PHP Fiber.
- Transpiler JsToPhp — i corpi con molte operazioni numeriche o variabili locali (incluse le letture di
obj.method(),new Ctor()ethis.member) vengono compilati in closure PHP che il JIT di tracciamento di PHP 8.5 trasforma in codice macchina nativo. - Cache JSON.stringify —
JSON.stringify(o)memorizza l'output sull'oggetto di input, invalidando transitivamente le versioni e accelerando l'idioma della copia profondaJSON.parse(JSON.stringify(o)).
Misurare il proprio carico di lavoro
php bench/run.php
Questo comando esegue la suite di microbenchmark inclusa in bench/microbench.js: alcuni carichi sintetici (calcoli nei cicli, funzioni ricorsive, creazione di oggetti, chiamate a metodi dei prototipi, push di array, andata e ritorno JSON e altro) e stampa il tempo mediano di esecuzione per test. È utile come controllo di coerenza; i numeri attuali vengono registrati in BENCH.md a ogni esecuzione dei benchmark in CI.
Per il proprio codice, sostituite lo script con uno qualsiasi:
php -d xdebug.mode=off bench/run.php path/to/your-script.js
Per l'analisi delle prestazioni a livello PHP, il profiler di Xdebug funziona con Phasis come con qualsiasi altra applicazione PHP. Eseguite uno script sotto xdebug.mode=profile ed esaminate l'output di cachegrind per vedere quali classi Phasis\ sono predominanti.
Roadmap
Cosa ridurrebbe maggiormente il divario con V8, in ordine approssimativo di fattibilità:
- Cache inline polimorfiche — consentire a un singolo punto di chiamata
LOAD_MEMBERdi mantenere in cache 2-4 strutture prima di ricorrere al dispatch lento. Utile per il codice che elabora ricevitori eterogenei. yield*e try-with-yield nel percorso dei generatori con snapshot del frame — queste strutture passano attualmente al tree-walker basato su Fiber. Ognuna richiede una gestione esplicita della macchina a stati (inoltro dell'iteratore delegato, stack dei gestori preservato durante la sospensione) prima di poter usare gli snapshot.- Generatori asincroni e
awaitsugli snapshot — la specifica combina la sospensione dei generatori con la coda di microtask. Quando il corpo in bytecode supporterà siayieldcheawait, anche i generatori asincroni potranno usare gli snapshot. - Inferenza dei tipi dei parametri in JsToPhp — i parametri delle funzioni sono impostati per impostazione predefinita sul tipo numerico, quindi
function (obj) { obj.method() }fa interrompere la compilazione. Inferire i parametri di tipo oggetto dal loro uso nel corpo consentirebbe le chiamate ai metodi sugli argomenti delle funzioni senza richiedere che il ricevitore venga creato localmente tramitenew. - Emissione diretta di opcache — saltare il passaggio
eval()di JsToPhp scrivendo direttamente opcode nel formato opcache. È l'attività più impegnativa; le altre sono realizzabili nell'architettura esistente.
Nessuna di queste funzionalità è necessaria per i carichi di lavoro di integrazione a cui mira Phasis.