İçeriğe geç

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ı, $thisValue olarak iletilerek dağıtılır.
  • new Ctor(args) kurucu çağrıları — LHS yerel değişkeninin türünü object olarak yükseltir ve nesne olmayan dönüşlere karşı Bailout koruması ekler.
  • Basit koşul şekillerine sahip for / while dö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):

  • with deyimleri.
  • 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 arguments değ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:

  1. 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.
  2. Derleme zamanında reddetme — gözlemlenebilir yan etkileri geri almayı gerektiren şekillerde (fn() durum değiştiren sayısal olmayan bir let 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 / storeNotifyHook iş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%XX yolu bulunan decodeURI / 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:

Kabuk
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.