Bayt Kodu VM'si
Varsayılan yürütme yolu bir ağaç yürüyücüsüdür — her AST düğümünü ziyaret eder ve bir değer döndürür. Milyonlarca kez çağrılan sık kullanılan işlevlerde, düğüm başına dağıtım ek yükü darboğaz oluşturur. Phasis bu ek yükü azaltmak için üzerine spekülatif bir bayt kodundan PHP closure'ına derleyici katmanı ekler.
VM ne zaman devreye girer
Her JsFunction için bir sık çağrı sayacı vardır. Sayaç eşiği (varsayılan 100) aşınca motor, src/Bytecode/JsToPhp.php aracılığıyla işlev gövdesini bir PHP closure'ına derlemeyi dener.
İşlev gövdesi bilinen bir şekil kümesine uyduğunda derleme başarılı olur:
- Öngörülebilir türlere (sayısal, nesne, işlev, dizi, dize) sahip yerel değişken atamaları.
- Sayılar üzerinde aritmetik işlemler.
- Nesne türündeki yerel değişkenlerde VE
thisüzerinde üye okumaları (this.v,obj.x). - Nesne türündeki yerel değişkenlerde yöntem çağrıları (
obj.method(args)); alıcı,$thisValueolarak iletilerek dağıtılır. new Ctor(args)kurucu çağrıları — LHS yerel değişkeninin türünüobjectolarak yükseltir ve nesne olmayan dönüşlere karşı Bailout koruması ekler.- Basit koşul şekillerine sahip
for/whiledöngüleri. - Hesaplanabilir ifadeli
return. - Tür denetimli koşullar üzerinde
if/else.
Gövde aşağıdakileri içerdiğinde derleme başarısız olur (ve işlev ağaç yürüyücüsünde kalır):
withdeyimleri.try / catch / finally(doğru tamamlama semantiği için ağaç yürüyücüsü tarafından işlenir).eval()çağrıları.- Doğrudan
argumentsdeğişikliği. - Derleyicinin henüz alt düzeye çevirmeyi bilmediği her şey.
Derleme başarılı olduğunda motor, işlevin çağrı yolunu derlenmiş closure ile değiştirir. Sonraki çağrılar, AST düğümü başına değil, işlev çağrısı başına tek bir ek yükle yerel PHP çalıştırır.
Vazgeçmeler
Derlenmiş hızlı yol bazı varsayımlarda bulunur (sayılar sayı kalır, özellikler aynı şekilde kalır). Çalışma zamanında bir varsayım ihlal edilirse closure bir Bailout fırlatır ve o çağrının yürütmesi ağaç yürüyücüsünde yeniden başlar.
Belirtimin doğruluğu burada en çok önem taşır. Ağaç yürüyücüsü, başarısız hızlı yoldan hiçbir yan etki gözlemlemeden vazgeçilen çağrıyı baştan yürütebilmelidir. Phasis bunu iki şekilde sağlar:
- Saf önekler — derleyici, spekülasyondan önce yan etkileri tarar. İşlev spekülasyon noktasından önce G/Ç (console.log, ana sistem işlevleri) yapıyorsa hızlı yol bunu atlamaz; işlemi tamamlar ve devam eder.
- Derleme zamanında reddetme — gözlemlenebilir yan etkileri geri almayı gerektiren şekillerde (
fn()durum değiştiren sayısal olmayan birlet r = fn()gibi), derleyici derlemeyi tamamen reddeder. İşlev ağaç yürüyücüsünde kalır.
Örnek durum: sayısal olmayan bir atama hızlı yolda fn() yürütür, Bailout fırlatır ve sonra ağaç yürüyücüsünde yeniden yürütülür — böylece fn() iki yan etki kümesiyle iki kez çalışır. Derleme zamanında reddetme, fn() işlevinin hızlı yolda hiç çalışmamasını sağlar; böylece ağaç yürüyücüsü bu çağrıyı yalnızca bir kez gözlemler.
VM dağıtımı (ikinci katman)
Bazı işlevler PHP closure'larına derlenir; diğerleri src/Bytecode/VM.php içindeki daha basit bir bayt kodu yorumlayıcısına geçer. Bayt kodu, switch dağıtımının işlediği düz bir işlem kodu dizisidir (LOAD_LOCAL, STORE_PROP, CALL_METHOD, …).
VM; PHP closure'ına derlenmeye değmeyecek, ancak AST dolaşımını atlamaktan yarar sağlayacak kadar sık kullanılan işlevler içindir. Yerleşik nesnelerdeki yöntem çağrıları (Array.prototype.map, String.prototype.split, …), genel callFunction trampoline'ini atlayan VM hızlı yollarından geçer.
Özel çağrı yığını
Op::CALL, Op::CALL_METHOD ve Op::NEW_CALL, JS çağrı yığınının tamamını VM::execute içinde tutan satır içi çağrı yolunu kullanır — her JS çağrısı için yeni bir PHP çerçevesi oluşturulmaz. Girişte, çağıranın durumu (pc, kod akışı, işlenen yığını, yerel değişkenler, env, this, katı mod bayrağı), VM başına SavedFrame örneklerinden oluşan bir savedFrames havuzuna aktarılır ve dağıtım döngüsü yerel değişkenlerini çağrılana göre yeniden düzenler. Op::RET kayıtlı çerçeveyi alıp durumu geri yükler. İstisnalar, ağaç yürüyücüsüne dönmeden önce kayıtlı çerçeveler boyunca açılır.
Uygunluk JsFunction üzerinde önbelleğe alınır: normal çağrılar için vmInlineCallCache (BC derlemeli gövde, bind() ile bağlanmamış, yerel değil), new ifadeleri için vmInlineConstructCache (ek olarak isConstructable, türetilmemiş; örnek alanı veya özel yöntem başlatıcısı yok). Diğer her şey standart callFunction / vmNewExpression trampoline'inden geçmeye devam eder.
Mimarinin ek bir yararı: PHP yığını sınırı artık JS özyineleme derinliğini kısıtlamaz. Artık tek sınır CallStack::maxDepth — 4 096 olarak ayarlanmıştır; tüm geçerli test262 durumlarının (çoğu 2 k çerçevenin altında kalır) çok üzerinde, patolojik sonsuz özyineleme testlerinin çalıştırıcı zaman aşımı bütçesi içinde hızla başarısız olacağı kadar düşüktür. Transpile edilmiş closure yolu (JsToPhp), satır içine alınmayan yedek yol için kendine ait 8 192 çerçevelik sınıra sahiptir.
Çerçeve anlık görüntülü oluşturucular
Basit oluşturucu gövdeleri — yield* yok, try/catch/finally içinde yield yok, await yok — bayt koduna derlenir. Derleyici, gövdenin askıya alındığı yere yeni bir Op::YIELD işlem kodu ekler; Op::YIELD dağıtım durumunu (cf, pc+1, yığın, sp, yerel değişkenler, env, thisValue, strict) bir GeneratorSnapshot yığın nesnesine kaydeder ve VM::execute üzerinden bir YieldResult sentinel değeri döndürür. Sürücü JsGenerator, anlık görüntüyü saklar ve sonraki next() / throw() çağrısında VM::execute nesnesine geri iletir; işlenen yığınının tepesindeki devam değerinin yerini alır.
Bu, desteklenen alt kümede PHP-Fiber yolunun yerini alır. Fiber tabanlı sürücü (executeGeneratorBody), derleyicinin reddettiği şekiller için hâlâ kullanılır — yield* delegasyonu, try bloğu içinde yield yapan oluşturucular, asenkron oluşturucular, await içeren asenkron işlevler. JsGenerator çift kipli çalışır: her next() / throw() / return(), örnekte anlık görüntü mü yoksa Fiber mı bulunduğunu denetler ve uygun biçimde yönlendirir.
Tezgâhta ayarlanabilir bir düğme yoktur; uygunluk derleme zamanında belirlenir ve JsFunction üzerinde önbelleğe alınır. Kazanç, anlık görüntü yolundaki her yield için bir PHP bağlam geçişinin azalmasıdır; Fiber yolu diğer her şey için değişmeden kalır.
Satır içi önbellekler
Op::LOAD_MEMBER, prototip kimliğine göre anahtarlanan ve her PC için kullanılan bir satır içi önbellek taşır; bu önbellek prototip yönteminin örnekte kullanıldığı örüntüde (c.sum(...), arr.push(...), tüm sınıf yöntemi dağıtımları) kullanılır. İsabet koşulu: alıcıda bu ad için kendine ait bir yuva bulunmaması, önbellekteki prototip başvurusunun alıcının getPrototype() değeriyle aynı olması ve prototipin PropertyMap::$version değerinin yakalanan sürümle hâlâ eşleşmesi.
Önbellek geçersizleştirme PropertyMap::$version üzerinden yürütülür. Yapısal değişiklikler (ayarlama / silme) bunu koşulsuz artırır. Veri yuvası değerlerinin üzerine yazma ise yalnızca PropertyMap::$isUsedAsProto true olduğunda artırır — nesne başka bir nesnenin [[Prototype]] değeri olarak ilk kez gözlemlendiğinde etkinleştirilen bir yazma bariyeri bayrağı. Asla prototip görevi görmeyen örnek nesneleri, her obj.x = v işleminde satır içi önbellek altyapısının maliyetini ödemez.
Getter erişimcileri hiçbir zaman önbelleğe alınmaz (her okumada yeniden çalışmaları gerekir).
PropertyMap, her yazmada koşulsuz artırılan ikinci bir sayaç olan $mutationVersion değerini taşır (yazma bariyeri yoktur). JSON.stringify önbelleği, ziyaret edilen her iç içe nesne/dizide bu sayacı sabitler; böylece o.a = 99 gibi yalnızca örneği etkileyen bir değişiklik, satır içi önbelleğin $version artırmayı atlamış olabileceği durumlarda bile önbelleğe alınmış dizeyi doğru biçimde geçersiz kılar.
JSON.stringify önbelleği
JsObject, $jsonCacheString + $jsonCacheVersions değerlerini taşır (önemsiz dizeleştirme sırasında ziyaret edilen her iç içe nesne için bir [obj, mutationVersion] çifti listesi). Yerine koyucu / boşluk karakteri olmadan yapılan sonraki JSON.stringify(o) çağrısında önemsiz hızlı yol, sabitlenen tüm sürümlerin değişmediğini doğrular ve yeniden dolaşıp kodlamadan önbelleğe alınmış dizeyi döndürür. JSON.parse(JSON.stringify(o)) gibi sık yinelenen derin klonlama örüntüleri, dizeleştirme aşamasında yaklaşık 2× hızlanır.
Önbellek, eşleşme olmadığında (sabitlenen sürümlerden herhangi biri farklıysa) sessizce silinir ve sonraki dolaşımda yeniden doldurulur. toJSON, getter'lar, erişimciler, simgeler veya [[PrimitiveValue]] yuvaları bulunan nesneler belirtime uygun serileştiriciye geçer ve önbelleği hiçbir zaman doldurmaz — dolayısıyla önbellekli yol yalnızca yapısal olarak tek biçimli verileri görür.
Yerleşik hızlı yollar
En büyük performans kazanımları, VM'ye belirli yerleşikleri öğreterek elde edildi:
Atomics.load/store/compareExchange,loadSpinHook/storeNotifyHookişbirlikçi zamanlamayla — 4 Atomics spin döngüsü testini yeniden çalışır hâle getirdi.- DST önbelleği için doğrudan hashmap kullanan
Date.prototype.getTimezoneOffset— SpiderMonkey DST stresinde %45 hızlanma. - Satır içi ASCII hızlı yolu kullanan
String.fromCharCode— tipik bir taramada yaklaşık 1 M dağıtım tasarrufu. - Toplu
hex2binçözümleme kullanan, 12 karakterlik ultra hızlı%XX%XX%XX%XXyolu bulunandecodeURI/decodeURIComponent. - Doğrudan özellik haritası erişimi kullanan
Object.keys/Object.values/Object.entries. - Alttaki PHP dizisi üzerinde yerel yineleme kullanan
Array.prototype.map/filter/forEach/reduce.
Her hızlı yol bir "şekil denetimi" ile korunur: alıcı beklenen şekle uymuyorsa (örneğin düz bir PHP string değeriyle desteklenmeyen bir TypedArray görünümü), VM belirtime uygun yavaş yola geçer.
Bu özellik ne yapmaz
Bayt kodu VM'si, V8 anlamında bir JIT değildir. İşlev başına yerel makine kodu üretmez ve satır içi önbelleğin tek biçimli prototip kimliği denetiminin ötesinde, gözlemlenen türlere göre spekülasyon yapmaz. İşlev sınırlarını aşan satır içi yerleştirme, çağrılan işlevin gövdesini çağıranın bayt koduna indirgemek yerine, özel çağrı yığınıyla (her JS çağrısı için fazladan PHP çerçevesi oluşturmadan) yaklaşık olarak sağlanır.
JsToPhp aktarıcısı, motorun JIT'e en çok yaklaşan parçasıdır: sık kullanılan işlev gövdeleri için PHP kaynak kodu üretir; ardından PHP 8.5'in izleme JIT'i bunu yerel koda derler. JsToPhp, eval() üzerinden çalışır; bu nedenle bir defalık derleme maliyetine katlanır ve izleme JIT'i işlevi kullanıcı koduymuş gibi keşfetmelidir. Doğrudan opcache işlem kodları üreterek eval() adımını atlamak aradaki farkı daha da kapatır — bkz. Kıyaslamalar § Yol Haritası.
Kıyaslama
bench/microbench.js, bir düzineden biraz fazla küçük kıyaslama çalıştırır (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). Ortanca geçen süre:
php bench/run.php
Güncel sayılar her bench iş akışı çalıştırmasından sonra BENCH.md deposuna kaydedilir. bench ve test262 matris iş akışları elle başlatılır.