Hızlı dinamik dil yorumlayıcıları nasıl yapılır
(zef-lang.dev)- AST’yi doğrudan dolaşan yorumlayıcılar da yalnızca değer gösterimi, inline cache, nesne modeli, watchpoint ve tekrarlanan ayrıntı optimizasyonlarıyla büyük performans artışı sağlayabilir
- Performans neredeyse hiç düşünülmeden yazılan Zef baseline, CPython 3.10’dan 35 kat, Lua 5.4.7’den 80 kat, QuickJS-ng 0.14.0’dan 23 kat daha yavaştı; ancak 21 optimizasyon adımının ardından 16.646 kat hızlanma sağladı
- En büyük sıçrama, nesne modelinin yeniden tasarlanmasıyla inline cache’in birleştirilmesinden geldi; Storage ve Offsets tabanlı erişim, önbelleğe alınmış AST özelleştirmesi ve ad override’larını izlemek için watchpoint uygulanmasıyla buna ek olarak 4.55 kat iyileşme sağlandı
- Ek iyileştirmeler arasında string tabanlı dispatch’in kaldırılması, Symbol eklenmesi, argüman aktarım yapısının değiştirilmesi, getter ve setter özelleştirmesi, hash table kısa yolu, dizi literal’leri ile
sqrtvetoStringözelleştirmesinin kademeli olarak uygulanması yer aldı - Yolo-C++ portu da dahil edildiğinde, baseline’a göre 66.962 kat daha hızlı sonucu elde edildi; CPython 3.10’dan 1.889 kat ve QuickJS-ng 0.14.0’dan 2.968 kat daha hızlı olsa da, bellek serbest bırakma olmadığı için uzun süre çalışan iş yükleri için uygun değil
Giriş ve değerlendirme metodolojisi
- Optimizasyon hedefi, AST’yi doğrudan dolaşan bir yorumlayıcı ve amaç, hobi olarak yapılan dinamik dil Zef’i Lua, QuickJS ve CPython ile rekabet edebilecek seviyeye çıkarmak
- JIT derleyiciler veya olgun GC’lerin ince ayarlarından ziyade, temelsiz bir başlangıç noktasında da uygulanabilecek optimizasyonlara odaklanılıyor
- Ele alınan teknikler değer gösterimi, inline caching, nesne modeli, watchpoint ve sağduyulu optimizasyonların tekrar tekrar uygulanması
- Yalnızca metindeki tekniklerle bile SSA, GC, bytecode veya makine kodu olmadan büyük performans artışı elde edildi
- Metindeki kapsamda 16 kat hızlanma
- Tamamlanmamış Yolo-C++ portu dahil edildiğinde 67 kat hızlanma
- Performans değerlendirmesinde ScriptBench1 benchmark paketi kullanıldı
- Dahil edilen benchmark’lar: Richards OS scheduler, DeltaBlue constraint solver, N-Body fizik simülasyonu, Splay ikili ağaç testi
- JavaScript, Python ve Lua için mevcut portlar kullanıldı
- Splay’in Python ve Lua portları Claude ile üretildi
- Deney ortamı: Ubuntu 22.04.5, Intel Core Ultra 5 135U, 32GB RAM, Fil-C++ 0.677
- Lua 5.4.7, GCC 11.4.0 ile derlendi
- QuickJS-ng 0.14.0 için GitHub releases ikilisi kullanıldı
- CPython 3.10 olarak Ubuntu’nun varsayılan sürümü kullanıldı
- Tüm deneylerde rastgele karıştırılmış 30 çalıştırmanın ortalaması kullanıldı
- Karşılaştırmaların çoğu, Fil-C++ ile derlenen Zef yorumlayıcısı ile Yolo-C derleyicisiyle derlenen diğer yorumlayıcılar arasında yapıldı
Orijinal Zef yorumlayıcısı
- Neredeyse hiç performans düşünülmeden yazıldı ve performans odaklı sadece iki tercih yapıldığı özellikle belirtildi
-
Değer gösterimi
- 64 bit tagged value kullanıldı
- Taşıyabildiği değerler: double, 32 bit tamsayı,
Object*
- Taşıyabildiği değerler: double, 32 bit tamsayı,
- double değerler
0x1000000000000offset’i yöntemiyle gösterildi- JavaScriptCore’dan öğrenilen bir teknik olarak tanıtılıyor
- Literatürde NuN tagging olarak anılıyor
- Tamsayılar ve pointer’lar native gösterimlerini kullanıyor
- Pointer değerlerinin
0x100000000’dan küçük olmadığı varsayımına dayanıyor - Bunun riskli bir seçim olduğu açıkça belirtiliyor
- Alternatif olarak tamsayılar için
0xffff000000000000üst bit etiketi kullanılabileceği söyleniyor
- Pointer değerlerinin
- Bu gösterimle sayısal işlemlerde bit testi tabanlı hızlı yol uygulanabiliyor
- Daha önemli avantaj, sayılar için heap allocation’ın önlenmesi
- Yeni bir yorumlayıcı yapılırken temel değer gösteriminin en başta doğru seçilmesi önemli; sonradan değiştirmek çok zor
- Dinamik tür dilleri için başlangıç noktası olarak 32 bit veya 64 bit tagged value öneriliyor
- 64 bit tagged value kullanıldı
-
Uygulama dili seçimi
- Yeterli düzeyde optimizasyonu ifade edebilen bir dil olarak C++ ailesi seçildi
- Java’nın düşük seviyeli optimizasyon tavanı nedeniyle tercih edilmeyeceği belirtiliyor
- Rust’ın, GC dili uygulaması için gereken global mutable state ve döngüsel referanslar içeren heap gösterimi yüzünden tercih edilmediği söyleniyor
- Çok dilli bir yapıyı göze almak veya bol miktarda
unsafekoda izin vermek halinde, kısmen ya da tamamen Rust kullanma ihtimalinden bahsediliyor
- Çok dilli bir yapıyı göze almak veya bol miktarda
-
Performans mühendisliği açısından kötü tercihler
- Fil-C++ kullanımı
- Hızlı geliştirme imkânı sağladı ve GC’yi bedavaya sundu
- Bellek güvenliği ihlallerini tanısal bilgi ve stack trace ile raporluyor
- Tanımsız davranış yok
- Performans maliyeti genelde yaklaşık 4 kat
- Özyinelemeli AST yürüyen yorumlayıcı
- Birçok yerde override edilen virtual
Node::evaluatemetod yapısı
- Birçok yerde override edilen virtual
- Aşırı string kullanımı
GetAST düğümü, değişken adını açıklayan birstd::stringsaklıyor- Her değişken erişiminde bu string kullanılıyor
- Aşırı hash table kullanımı
Getçalıştırıldığında string anahtarlastd::unordered_maparaması yapılıyor
- Özyinelemeli çağrı zinciri tabanlı scope araması
- Neredeyse her yapının iç içe geçmesine ve closure’lara izin veriliyor
- F fonksiyonu içindeki A sınıfı ve B sınıfı içindeki G fonksiyonu gibi bir iç içe yapıda, A’nın metodları A alanlarını, F yerel değişkenlerini, B alanlarını ve G yerel değişkenlerini görebiliyor
- Orijinal uygulama bunu, farklı scope nesnelerini sorgulayan C++ özyinelemeli fonksiyonlarıyla ele alıyordu
- Fil-C++ kullanımı
-
Orijinal uygulamanın özellikleri
- Kötü seçimlere rağmen az kodla oldukça karmaşık bir dil yorumlayıcısı yazılabildi
- En büyük modül parser
- Geri kalanı ise basit ve anlaşılır
-
Başlangıç performansı
- Orijinal yorumlayıcı CPython 3.10’dan 35 kat daha yavaş
-
Lua 5.4.7’den 80 kat daha yavaş
- QuickJS-ng 0.14.0’dan 23 kat daha yavaş
Genel optimizasyon ilerleme tablosu
- Tablo, Zef Baseline’dan Zef Change #21: No Asserts’e ve Zef in Yolo-C++’a kadar performans değişimini özetliyor
- Karşılaştırma sütunları: vs Zef Baseline, vs Python 3.10, vs Lua 5.4.7, vs QuickJS-ng 0.14.0
- Son satıra göre Zef Change #21: No Asserts, baseline’a kıyasla 16.646 kat daha hızlı
-
Python 3.10’dan 2.13 kat daha yavaş
-
Lua 5.4.7’den 4.781 kat daha yavaş
- QuickJS-ng 0.14.0’dan 1.355 kat daha yavaş
-
-
Zef in Yolo-C++**, baseline’a kıyasla 66.962 kat daha hızlı
-
Python 3.10’dan 1.889 kat daha hızlı
-
Lua 5.4.7’den 1.189 kat daha yavaş
- QuickJS-ng 0.14.0’dan 2.968 kat daha hızlı
-
İlk optimizasyon aşaması
-
Optimizasyon #1: Operatörleri doğrudan çağırma
- Artık parser operatörleri operatör adına sahip
DotCalldüğümleri olarak üretmek yerine, her operatör için ayrı AST düğümleri oluşturuyor - Zef'te
a + bilea.add(b)aynıdır- Önceden
a + b,DotCall(a, "add")ve argümanbolarak parse ediliyordu - Her aritmetik işlemde operatör metot adı dizgesine bakma maliyeti oluşuyordu
DotCall, dizgeyiValue::callMethod'a iletiyorduValue::callMethod, çoklu dizge karşılaştırmaları yapıyordu
- Önceden
- Değişiklikten sonra parser
Binary<>veUnary<>düğümleri oluşturuyor- Template ve lambda'lar kullanılarak her operatör için farklı
Node::evaluateoverride'ları sağlanıyor - Her düğüm, ilgili operatörün
Valuehızlı yolunu doğrudan çağırıyor - Örneğin
a + b,Binary<add için lambda>::evaluateçağrısının ardındanValue::addçağrısını yapıyor
- Template ve lambda'lar kullanılarak her operatör için farklı
- Performans etkisi %17,5 iyileşme
- Bu noktadaki performans CPython 3.10'dan 30 kat daha yavaş
- Lua 5.4.7'den 67 kat daha yavaş
- QuickJS-ng 0.14.0'dan 19 kat daha yavaş
- Artık parser operatörleri operatör adına sahip
-
Optimizasyon #2: RMW operatörlerini doğrudan çağırma
- Genel operatörler hızlandı, ancak
a += bgibi RMW biçimleri hâlâ dizge tabanlı dispatch kullanıyordu - Parser'ın her RMW durumu için ayrı düğümler üretmesi sağlandı
- Parser, LValue düğümünden
makeRMWsanal çağrısı aracılığıyla kendisini RMW ile değiştirmesini istiyor - RMW'ye dönüşebilen LValue türleri Get, Dot ve Subscript
- Get,
iddeğişken okumasına karşılık geliyor - Dot,
expr.idiçin kullanılıyor - Subscript,
expr[index]için kullanılıyor
- Get,
- Her sanal çağrı
SPECIALIZE_NEW_RMWmakrosunu kullanıyor- SetRMW,
id += value - DotSetRMW,
expr.id += value - SubscriptRMW,
expr[index] += value
- SetRMW,
- Değişiklik #1'deki operatör özelleştirmesi lambda dispatch kullanıyordu
- RMW ise enum kullanıyor
- Bunun nedeni
get,dot,subscriptolmak üzere üç yolu da ele alması ve enum'un birden çok yere taşınmasının gerekmesi - Sonuçta gerçek RMW operatörü çağrı dispatch'ini
Value::callRMW<>template fonksiyonu yapıyor
- Bunun nedeni
- Performans etkisi %3,7 iyileşme
- Bu noktadaki performans CPython 3.10'dan 29 kat daha yavaş
- Lua 5.4.7'den 65 kat daha yavaş
- QuickJS-ng 0.14.0'dan 18,5 kat daha yavaş
- Başlangıç noktasına göre 1,22 kat daha hızlı
- Genel operatörler hızlandı, ancak
-
Optimizasyon #3: IntObject kontrolünden kaçınma
- Darboğaz,
Valuehızlı yolununisInt()kullanması ve bunun içindekiisIntSlow()'unObject::isInt()sanal çağrısı yapmasıydı - Başlangıçtaki değer gösteriminde dört durum vardı
- tagged int32
- tagged double
- int32 olarak temsil edilemeyen int64 için IntObject
- diğer tüm nesneler
- IntObject durumunda da tamsayı metodu dispatch'ini
Valueüstleniyordu- Bunun amacı tüm aritmetik işlem uygulamalarını tek bir yerde, yani
Valueiçinde tutmaktı
- Bunun amacı tüm aritmetik işlem uygulamalarını tek bir yerde, yani
- Optimizasyondan sonra
Valuehızlı yolu yalnızca int32 ve double'ı dikkate alıyor- IntObject işleme mantığı
IntObject'in kendisine taşındı - Her metot dispatch'inde oluşan
isInt()çağrısından kaçınıldı
- IntObject işleme mantığı
- Performans etkisi %1 iyileşme
- Bu noktadaki performans CPython 3.10'dan 29 kat daha yavaş
- Lua 5.4.7'den 65 kat daha yavaş
- QuickJS-ng 0.14.0'dan 18 kat daha yavaş
- Başlangıç noktasına göre 1,23 kat daha hızlı
- Darboğaz,
-
Optimizasyon #4: Symbol
- Başlangıçta yorumlayıcı,
std::stringi neredeyse her yerde kullanıyordu - Maliyeti yüksek dizge kullanım noktaları
Context::get,Context::set,Context::callFunction,Value::callMethod,Value::dot,Value::setDot,Value::callOperator<>,Object::callMethodailesiydi - Bu yapıda sadece basit bir hash tablosu bakışı değil, dizge anahtarlı hash tablosu bakışı yapılıyor ve çalışma sırasında dizge hashing'i ile karşılaştırmaları tekrar tekrar gerçekleşiyordu
- Optimizasyon, dizge tabanlı aramaları hash-consed
Symbolnesne işaretçileriyle değiştirdi - Yeni bir
Symbolsınıfı eklendi- Uygulama
symbol.hvesymbol.cppiçinde - Symbol ile dizge arasında çift yönlü dönüşüm yapılabiliyor
- Dizgeden Symbol'e dönüşüm sırasında global hash tablosu ile hash consing yapılıyor
- Sonuç olarak aynı sembol olup olmadığı yalnızca
Symbol*işaretçi özdeşliği karşılaştırmasıyla belirlenebiliyor
- Uygulama
- Dizge literal'leri yerine önceden hazırlanmış semboller kullanılıyor
- Örneğin
"subscript"yerineSymbol::subscript
- Örneğin
- Çok sayıda fonksiyon imzası
const std::string&yerineSymbol*kullanacak şekilde değiştirildi - Performans etkisi %18 iyileşme
- Bu noktadaki performans CPython 3.10'dan 24 kat daha yavaş
- Lua 5.4.7'den 54 kat daha yavaş
- QuickJS-ng 0.14.0'dan 15 kat daha yavaş
- Başlangıç noktasına göre 1,46 kat daha hızlı
- Başlangıçta yorumlayıcı,
-
Optimizasyon #5: Value inline etme
- Temel fikir, kritik fonksiyonların inline edilmesine izin vermek
- Neredeyse tüm değişikliklerin merkezinde yeni başlık dosyası
valueinlines.hyer alıyor - Bunun
value.hyerine ayrı bir başlıkta tutulmasının nedeni,value.h'ı include etmesi gereken başlıklar tarafından kullanılması - Performans etkisi %2,8 iyileşme
- Bu noktadaki performans CPython 3.10'dan 24 kat daha yavaş
- Lua 5.4.7'den 53 kat daha yavaş
- QuickJS-ng 0.14.0'dan 15 kat daha yavaş
- Başlangıç noktasına göre 1,5 kat daha hızlı
Nesne modeli ve önbellek yapısının yeniden tasarlanması
-
Optimizasyon #6: nesne modeli, inline cache, Watchpoint
Object,ClassObject,Contextçalışma biçimleri büyük ölçüde yeniden düzenlenerek nesne ayırma maliyeti düşürüldü ve erişim sırasında hash tablosu sorgusundan kaçınıldı- Bu değişiklik, nesne modeli, inline cache ve watchpoint olmak üzere üç özelliğin birleşiminden oluşuyor
-
Nesne modeli
- Önceden her leksiksel scope için bir
Contextnesnesi ayrılıyordu- Her
Context, o scope’taki değişkenleri tutan bir hash tablosu barındırıyordu
- Her
- Nesneler daha karmaşık bir yapıdaydı
- Her nesne, örneği olduğu sınıfları
Contexte eşleyen bir hash tablosu tutuyordu
- Her nesne, örneği olduğu sınıfları
- Bu yapının gerekli olmasının nedeni kalıtım ve iç içe scope’lar
Bar,Foodan kalıtım aldığındaBarveFoofarklı scope’ları closure olarak yakalayabiliyor- Ayrıca aynı ada sahip farklı private alanlara da sahip olabiliyorlar
- Yeni yapı
Storagekavramını tanıtıyor- Veri, offset’lere göre depolanıyor
- offset ise hangi
Contexttarafından belirlendiğine göre tanımlanıyor
Contexthâlâ varlığını koruyor ancak nesne ya da scope oluşturulurken değil, AST’ninresolvegeçişinde önceden oluşturuluyor- Gerçek nesne veya scope oluşturulurken ise ilgili
Contextin hesapladığı boyuta göre yalnızca Storage ayrılıyor
- Önceden her leksiksel scope için bir
-
Inline cache
expr.namegibi bir kod konumunda, en son görülenexprin dinamik türünü venamein çözümlendiği son offset’i hatırlayan bir teknik- Genellikle JIT bağlamında anlatılan klasik bir teknik olsa da burada yorumlayıcıya uygulanıyor
- Hatırlanan bilgi, sıradan AST düğümü üzerine placement construct ile özelleştirilmiş AST düğümleri yerleştirilerek uygulanıyor
-
Inline cache bileşenleri
CacheRecipe- Belirli bir erişimin ne yaptığını ve önbelleğe alınıp alınamayacağını izliyor
Context,ClassObject,PackagegenelindeCacheRecipeçağrıları eklenmiş- Erişim sürecine dair bilgi toplanıyor
Dot::evaluategibi AST değerlendirme fonksiyonları, yürüttükleri çok biçimli işlemlerden elde ettikleriCacheRecipe’yithisile birlikteconstructCache<>fonksiyonuna iletiyorconstructCacheCacheRecipeye göre yeni AST düğümü özelleştirmeleri derliyor- Şablon mekanizmasıyla çeşitli özelleştirilmiş AST düğümleri üretiyor
- Yerel değişken erişimiyse, aktarılan
storageüzerinde doğrudan yükleme yapıyor - En son görülen sınıfla aynı olup olmadığını kontrol eden bir class check gerçekleştiriyor
- Ardından en son görülen fonksiyona doğrudan fonksiyon çağrısı yapıyor
- Gerekirse chain step ve watchpoint kombinasyonu kullanıyor
- Önbelleğe alınabilen AST düğümlerinin her biri kendi cached variant’ını barındırıyor
- Önce
cachenesnesiyle hızlı çağrı deneniyor cachenesnesinin türüconstructCache<>tarafından belirleniyor
- Önce
-
Watchpoint
- Leksiksel scope’ta
xdeğişkeni bulunduğu ve bunun içindeFoosınıfı yer aldığı,Foometodunun daxe eriştiği bir örnek veriliyor Fooiçindexadında bir fonksiyon ya da değişken yoksa doğrudan dıştakixokunabiliyormuş gibi görünüyor- Ancak bir alt sınıf
xadında bir getter ekleyebilir - Bu durumda erişimin sonucu dıştaki
xdeğil, getter olmalıdır - Inline cache, böyle bir değişiklik olasılığını ele almak için çalışma zamanında
Watchpointkuruyor - Örnekte, bu adın override edilip edilmediğini izleyen bir watchpoint kullanılıyor
- Leksiksel scope’ta
-
Üç özelliğin aynı anda uygulanma nedeni
- Yeni nesne modeli tek başına, inline cache iyi çalışmıyorsa anlamlı bir iyileşme sağlamakta yetersiz kalıyor
- Inline cache de watchpoint olmadan birçok önbellek koşulunu güvenli biçimde ele alamadığı için pratikte sınırlı fayda sağlıyor
- Yeni nesne modeli ile watchpoint birlikte iyi çalışmak zorunda
-
Uygulama süreci ve zor kısımlar
- Başlangıçta basit bir
CacheRecipesürümü yazıldı; ayrıca nihai biçime yakın Storage ve offset tasarımıyla ilerlenmeye başlandı - En zor işlerden biri intrinsic class uygulama biçimini değiştirmekti
- Dizi örneği
- Önceden
ArrayObject::tryCallMethod, tüm metodlarıObject::tryCallMethodsanal çağrısını yakalayarak uyguluyordu - Yeni nesne modelinde
Objectiçinde ne vtable var ne de sanal metodlar - Bunun yerine
Object::tryCallMethod,object->classObject()->tryCallMethod(object, ...)çağrısına delege ediyor - Bu yüzden
Arraymetodlarını sunmak için bu metodları barındıran Array’e özel sınıfın kendisini oluşturmak gerekiyor
- Önceden
- Sonuç olarak intrinsic işlevlerin önemli bir bölümü, uygulamanın geneline dağılmış yapıdan çıkıp
makerootcontext.cppmerkezli bir yapıya taşındı - Bunun olumlu bir sonuç olarak görülmesinin nedeni, nesnelerin native/intrinsic fonksiyonlarına da inline cache’in aynen uygulanabilmesi
- Performans etkisi 4,55 kat iyileşme oldu
- Bu aşamadaki performans CPython 3.10’dan 5,2 kat daha yavaş
- Lua 5.4.7’den 11,7 kat daha yavaş
- QuickJS-ng 0.14.0’dan 3,3 kat daha yavaş
- Başlangıç noktasına göre 6,8 kat daha hızlı
- Fil-C++’ın kayıp farkının, diğer yorumlayıcılara kıyasla genel olarak Fil-C maliyeti seviyesine kadar daraldığı değerlendiriliyor
- Başlangıçta basit bir
Çağrı ve erişim yolu optimizasyonu
-
Optimizasyon #7: Argüman iletim yapısının iyileştirilmesi
- Değişiklikten önce Zef yorumlayıcısı fonksiyon argümanlarını
const std::optional<std::vector<Value>>&olarak iletiyordu optionalgerekmesinin nedeni, bazı uç durumlarda şu ikisini ayırmak zorunda olmasıydı:o.gettero.function()
- Zef'te çoğu durumda ikisi de fonksiyon çağrısıdır, ancak istisna olarak şu kod vardır:
o.NestedClasso.NestedClass()
- İlki
NestedClassnesnesinin kendisini döndürür - İkincisi instance oluşturur
- Bu yüzden argümansız fonksiyon çağrısı ile argüman dizisi boş olan getter benzeri çağrı ayrılmalıdır
- Ancak mevcut yapı verimsizdi
- Çağıran taraf
vectorallocate ediyor - Çağrılan taraf da bu vektörü kopyalayan bir arguments scope yeniden allocate ediyordu
- Çağıran taraf
- Değişiklik olarak
Argumentstipi eklendi- Biçimi, çağrılan tarafın oluşturduğu arguments scope ile tam olarak aynı
- Artık çağıran taraf doğrudan bu biçimde allocate ediyor
- Yolo-C++ tarafında da vector backing store
mallockaldırılarak allocation sayısı azaltıldı - Fil-C++'ta ise
std::optional'ın kendisi heap allocation yapıyorstd::optionalolmasa bileconst std::vector<>&iletmek de allocation yaratıyor- Stack allocation olan şeyin heap allocation olarak işaretlendiği belirtiliyor
- Ayrıca çağıran tarafın vektör boyutunu önceden ayarlamaması nedeniyle birden çok kez yeniden allocation oluştuğu da belirtiliyor
- Değişikliğin büyük kısmı, fonksiyon imzalarını
Arguments*ile değiştirmekten ibaretti - Performans etkisi 1,33 kat iyileşme
- Bu noktada performans CPython 3.10'dan 3,9 kat daha yavaş
- Lua 5.4.7'den 8,8 kat daha yavaş
- QuickJS-ng 0.14.0'dan 2,5 kat daha yavaş
- Başlangıç noktasına kıyasla 9,05 kat daha hızlı
- Değişiklikten önce Zef yorumlayıcısı fonksiyon argümanlarını
-
Optimizasyon #8: Getter özelleştirmesi
- Zef, Ruby'ye benzer şekilde instance field'ları varsayılan olarak private tutar
- Örnek:
class Foo { my f fn (inF) f = inF }- Constructor'da alınan değeri yalnızca instance'ın görebildiği yerel değişken
fiçine kaydeder
- Constructor'da alınan değeri yalnızca instance'ın görebildiği yerel değişken
- Aynı türün instance'ları bile başka nesnenin
falanına erişemez- Örnek:
fn nope(o) o.f println(Foo(42).nope(Foo(666)))nopeiçindekio.f,o'nunfalanına erişemez
- Örnek:
- Bunun nedeni, field'ların class üyelerinin scope chain'inde görünme biçimiyle çalışmasıdır
o.f, bir field okuması değilfadlı bir metodu çağırma isteğidir
- Bu yüzden şu kalıp sık görülür
my ffn f f- yani yerel değişken
f'yi döndüren, adıfolan bir metot
- Daha kısa sözdizimi olarak
readable fvardırmy fvefn f fiçin kısaltmadır
- Birçok metot çağrısı fiilen getter çağrısıdır
- Tüm getter'ların AST değerlendirerek çalışması israftır
- Optimizasyon getter özelleştirmesidir
- Merkezinde
UserFunctionvardır - Yeni
Node::inferGettermetodu ile fonksiyon gövdesinin basit bir getter olup olmadığı çıkarımlanır
- Merkezinde
- Çıkarım kuralları
Block::inferGetter, içerdiği her şey getter olarak çıkarımlanabiliyorsa kendisi de getter olarak çıkarımlanırGet::inferGetter, kendisini getter olarak çıkarımlar ve yüklenecek offset değerini döndürürContext::tryGetFieldOffsets, getter'ın çalışacağı lexical scope içinde o field'ın kesin olarak var olduğu durumda yalnızca boş olmayan birOffsetsdöndürürUserFunction, fonksiyon gövdesi getter olarak çıkarımlanabiliyorsa, bilinen offset'ten doğrudan okuma yapan özel birFunctionalt sınıfı olarak resolve edilir
- Performans etkisi %5,6 iyileşme
- Bu noktada performans CPython 3.10'dan 3,7 kat daha yavaş
- Lua 5.4.7'den 8,3 kat daha yavaş
- QuickJS-ng 0.14.0'dan 2,4 kat daha yavaş
- Başlangıç noktasına kıyasla 9,55 kat daha hızlı
-
Optimizasyon #9: Setter özelleştirmesi
- Setter çıkarımında
fn set_fieldName(newValue) fieldName = newValuekalıbını eşleştirmek gerekir UserFunction'ın çıkarım aşamasında setter'ın parametre adı aktarılmalıdırSet'in çıkarım aşamasında bunun ClassObject'e yazma işlemi olmadığının doğrulanması gerekir; ayrıca setter parametresinin set işleminin kaynağı olarak kullanılıp kullanılmadığı da kontrol edilmelidir- Performans etkisi %3,4 iyileşme
- Bu noktada Zef CPython 3.10'dan 3,6 kat daha yavaş
- Lua 5.4.7'den 8 kat daha yavaş
- QuickJS-ng 0.14.0'dan 2,3 kat daha yavaş
- Başlangıç noktasına kıyasla 9,87 kat daha hızlı
- Setter çıkarımında
-
Optimizasyon #10:
callMethodinline edilmesi- Önemli bir fonksiyon tek satırlık bir değişiklikle inline edildi
- Performans etkisi %3,2 iyileşme
- Bu noktada Zef CPython 3.10'dan 3,5 kat daha yavaş
- Lua 5.4.7'den 7,8 kat daha yavaş
- QuickJS-ng 0.14.0'dan 2,2 kat daha yavaş
- Başlangıç noktasına kıyasla 10,2 kat daha hızlı
-
Optimizasyon #11: Hash tablosu
- Metot çağrısında inline cache miss oluştuğunda
ClassObject::tryCallMethodveClassObject::TryCallMethodDirectyollarına inmek gerekiyordu; bu yolların ikisi de büyük ve karmaşıktı - Eski arama maliyeti hiyerarşi derinliğiyle orantılı O(hierarchy depth) idi
- Hiyerarşideki her class için, çağrının üye fonksiyon olarak çözümlenip çözümlenmediğini kontrol eden bir hash tablosu sorgusu yapılıyordu
- Hiyerarşideki her class için, çağrının nested class olarak çözümlenip çözümlenmediğini kontrol eden bir hash tablosu sorgusu da yapılıyordu
- Yeni değişiklikle, anahtarı receiver class ve symbol olan global bir hash tablosu eklendi
- Tek bir sorguyla callee doğrudan döndürülüyor
classobject.hiçinde, tümtryCallMethodSlowyoluna inmeden önce önce bu global tablo sorgulanıyorclassobject.cppiçinde başarılı sorgu sonuçları global tabloya kaydediliyor- Global hash tablosunun kendisi ise görece basit bir implementasyona sahip
- Performans etkisi %15 iyileşme
- Bu noktada Zef CPython 3.10'dan 3 kat daha yavaş
- Lua 5.4.7'den 6,8 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1,9 kat daha yavaş
- Başlangıç noktasına kıyasla 11,8 kat daha hızlı
- Metot çağrısında inline cache miss oluştuğunda
-
Optimizasyon #12:
std::optional'dan kaçınma- Fil-C++'ta union ile ilgili derleyici patolojileri nedeniyle
std::optionalheap allocation gerektiriyor - Normalde LLVM, union bellek erişim türlerini gevşek ele alır; ancak bu durum invisicaps ile çakışır
- Union içindeki pointer'ın, programcının bakış açısından öngörülmesi zor şekilde capability kaybettiği durumlar oluşur
- Sonuç olarak Fil-C'de programcı hatası olmadan bile null capability taşıyan bir nesneyi dereference etme paniği ortaya çıkabilir
- Bunu hafifletmek için Fil-C++ derleyicisi, union türündeki yerel değişkenleri işlerken LLVM'nin muhafazakâr davranmasını sağlamak amacıyla intrinsics ekler
- Sonrasında
FilPizlonatorgeçişi kendi escape analysis'ini yaparak union türündeki yerel değişkenleri register allocation yapılabilir hale getirmeye çalışır- Ancak bu analiz, genel LLVM'deki SROA analizinin tamlığına sahip değildir
- Sonuç olarak
std::optionalgibi union içeren class'ların iletimi, Fil-C++'ta sık sık bellek allocation'ına yol açar - Bu değişiklik, hot path içinde
std::optional'a giden kod yolunu kaçınarak geçiyor - Performans etkisi %1,7 iyileşme
- Bu noktada Zef CPython 3.10'dan 3 kat daha yavaş
- Lua 5.4.7'den 6,65 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1,9 kat daha yavaş
- Fil-C++'ta union ile ilgili derleyici patolojileri nedeniyle
-
Başlangıç noktasına kıyasla 12 kat daha hızlı
-
Optimizasyon #13: özelleştirilmiş argümanlar
- Zef’in tüm built-in fonksiyonları 1 veya 2 argüman alır ve yerel implementasyonda bunları taşımak için
Argumentsnesnesi tahsis etmeye gerek yoktur - setter da her zaman tek bir argüman alır ve setter çıkarımı yapıldığında özelleştirilmiş setter implementasyonu da
Argumentsnesnesi olmadan yalnızca değer argümanını doğrudan alması yeterlidir - Bu değişiklikle birlikte
ZeroArguments,OneArgument,TwoArgumentsözelleştirilmiş argüman türleri eklendi- callee buna ihtiyaç duymadığında caller
Argumentsnesnesi tahsisinden kaçınabilir
- callee buna ihtiyaç duymadığında caller
ZeroArguments,(Arguments*)nullptrile karışmaması için gereklidir- daha önce
(Arguments*)nullptr, getter çağrısı anlamında kullanılıyordu ve bu mantık korundu - artık
ZeroArguments, argümansız fonksiyon çağrısı anlamına geliyor
- daha önce
- Değişikliklerin büyük bölümü, argüman alan fonksiyonların templateleştirilmesinden oluşuyor
ZeroArguments,OneArgument,TwoArguments,Arguments*için ayrı ayrı açık instantiation yapıldı- mevcut kodun önemli bir kısmı argüman çıkarma yardımcısı olarak
Value::getArgkullanıyordu; buna özelleştirilmiş argüman overload’ları eklendi - argüman kullanan yerel koddaki değişiklikler görece doğrudan düzenlemelerdi
- Performans etkisi %3,8 iyileşme oldu
- bu noktada Zef, CPython 3.10’dan 2,9 kat daha yavaş
- Lua 5.4.7’den 6,4 kat daha yavaş
- QuickJS-ng 0.14.0’dan 1,8 kat daha yavaş
- başlangıç noktasına kıyasla 12,4 kat daha hızlı
- Zef’in tüm built-in fonksiyonları 1 veya 2 argüman alır ve yerel implementasyonda bunları taşımak için
Fil-C patolojisini aşma ve ince taneli özelleştirme
-
Optimizasyon #14: iyileştirilmiş Value slow path
- Bir başka Fil-C patolojisi aşma yöntemiyle büyük hız artışı elde edildi
- Değişiklikten önce
Valueiçin out-of-line slow path,Value'nin bir üye fonksiyonuydu ve örtük birconst Value*argümanı gerektiriyordu - Bu yapıda caller'ın
Value'yu stack üzerinde ayırması gerekiyordu - Fil-C++'ta tüm stack ayırmaları heap ayırmasıdır
- Bu yüzden slow path'i çağıran kod,
Value'yu heap üzerinde ayırıyordu
- Bu yüzden slow path'i çağıran kod,
- Değişiklikten sonra bu metotlar
staticyapıldı veValue, değer olarak geçirildi- Sonuç olarak ayrı bir ayırma gerekmiyor
- Performans etkisi %10 iyileşme
- Bu noktada Zef, CPython 3.10'dan 2.6 kat daha yavaş
- Lua 5.4.7'den 5.8 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1.65 kat daha yavaş
- Başlangıç noktasına göre 13.6 kat daha hızlı
-
Optimizasyon #15:
DotSetRMWyinelemelerini kaldırma- Bir miktar yinelenen kod kaldırıldı
constructCache<>tarafından özelleştirilen şablon fonksiyonlarda makine kodunun küçülmesinin faydalı olabileceği düşünüldü- Gerçek sonuçta performansa etkisi olmadı
-
Optimizasyon #16:
sqrtözelleştirmesi- Inline cache, çağrıları istenen fonksiyona iyi yönlendiriyor ama yalnızca nesnelerde çalışıyor
- Nesne olmayan durumlarda
Binary<>,Unary<>,Value::callRMW<>fast path'i, alıcınınintveyadoubleolup olmadığını kontrol etmeye dayanıyor - Bu yaklaşım yalnızca parser'ın tanıdığı operatörler için geçerli
value.sqrtgibi biçimlerde uygulanamıyor
- Bu değişiklikle
Dot,value.sqrtiçin özelleştirilebilir hale geldi - Performans etkisi %1.6 iyileşme
- Bu noktada Zef, CPython 3.10'dan 2.6 kat daha yavaş
- Lua 5.4.7'den 5.75 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1.6 kat daha yavaş
- Başlangıç noktasına göre 13.8 kat daha hızlı
-
Optimizasyon #17:
toStringözelleştirmesi- Önceki optimizasyondakiyle neredeyse aynı yöntemle
toStringözelleştirmesi uygulandı - Bu değişiklik,
int'i string'e dönüştürürken oluşan ayırma sayısını azaltan mantığı da içeriyor - Performans etkisi %2.7 iyileşme
- Bu noktada Zef, CPython 3.10'dan 2.5 kat daha yavaş
- Lua 5.4.7'den 5.6 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1.6 kat daha yavaş
- Başlangıç noktasına göre 14.2 kat daha hızlı
- Önceki optimizasyondakiyle neredeyse aynı yöntemle
-
Optimizasyon #18: dizi literal özelleştirmesi
my whatever = [1, 2, 3]gibi kodlarda Zef'te diziler alias edilebilir ve mutable olduğu için yeni bir dizi ayırmak gerekiyor- Değişiklikten önce her çalıştırmada AST boyunca aşağı inilip
1,2,3her seferinde yeniden özyinelemeli olarak değerlendiriliyordu - Bu değişiklik,
ArrayLiteraldüğümünü sabit dizi ayırma durumu için özelleştiriyor - Performans etkisi %8.1 iyileşme
- Bu noktada Zef, CPython 3.10'dan 2.3 kat daha yavaş
- Lua 5.4.7'den 5.2 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1.5 kat daha yavaş
- Başlangıç noktasına göre 15.35 kat daha hızlı
-
Optimizasyon #19:
Value::callOperatoriyileştirmesi- Daha önce
Value'yu referansla geçirmeyerek hız kazanımı sağlayan optimizasyonun aynısı,callOperatorslow path için de uygulandı - Performans etkisi %6.5 iyileşme
- Bu noktada Zef, CPython 3.10'dan 2.2 kat daha yavaş
- Lua 5.4.7'den 4.9 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1.4 kat daha yavaş
- Başlangıç noktasına göre 16.3 kat daha hızlı
- Daha önce
-
Optimizasyon #20: daha iyi C++ seçenekleri
- Fil-C++'ta gereksiz RTTI ve libc++ hardening devre dışı bırakıldı
- C++ kodunun kendisinde değişiklik yok; yalnızca build system yapılandırması değişti
- Performans etkisi %1.8 iyileşme
- Bu noktada Zef, CPython 3.10'dan 2.1 kat daha yavaş
- Lua 5.4.7'den 4.8 kat daha yavaş
- QuickJS-ng 0.14.0'dan 1.35 kat daha yavaş
- Başlangıç noktasına göre 16.6 kat daha hızlı
-
Optimizasyon #21: assert'leri devre dışı bırakma
- Son optimizasyon olarak assertion'ların varsayılan olarak devre dışı bırakılması uygulandı
- Mevcut kod, Fil-C'ye özel
ZASSERTmakrosunu kullanıyordu- Bu yapı assert'leri her zaman çalıştırıyordu
- Değişiklikten sonra dahili
ASSERTmakrosu kullanıldı- Assert'ler yalnızca
ASSERTS_ENABLEDayarlıysa çalışıyor
- Assert'ler yalnızca
- Bu değişiklik, kodun Yolo-C++ ile derlenebilmesini sağlayan başka düzeltmeleri de içeriyor
- Beklentinin aksine hiçbir hız artışı olmadı
Yolo-C++ sonuçları ve sınırlamalar
- Kodun Yolo-C++ ile derlenmesi sonucu 4 kat hız artışı elde edildi
- Ancak bu yaklaşım sound değil ve suboptimal
- Sound olmamasının nedeni, mevcut Fil-C++ GC çağrılarının
callocçağrılarına dönüşmesi - Bunun sonucu olarak bellek serbest bırakılmıyor ve yeterince uzun süren iş yüklerinde yorumlayıcı bellek tükenmesine ulaşıyor
- ScriptBench1'de test süresi kısa olduğu için bellek tükenmesi yaşanmıyor
- Sound olmamasının nedeni, mevcut Fil-C++ GC çağrılarının
- Suboptimal olmasının nedeni, gerçek GC allocator'ının glibc 2.35'in
calloc'undan daha hızlı olması - Bu nedenle Yolo-C++ portuna gerçek GC eklenirse 4 kattan daha büyük bir hız artışının mümkün olabileceği belirtiliyor
- Bu deneyde GCC 11.4.0 kullanıldı
- Bu noktada Zef
-
CPython 3.10'dan 1.9 kat daha hızlı
-
Lua 5.4.7'den 1.2 kat daha yavaş
-
QuickJS-ng 0.14.0'dan 3 kat daha hızlı
- Başlangıç noktasına göre 67 kat daha hızlı
-
Ham benchmark verileri
- Benchmark çalışma süresi birimi saniye
- Tabloda her yorumlayıcı için
nbody,splay,richards,deltablue,geomeanyer alır -
Python 3.10
nbody0.0364splay0.8326richards0.0822deltablue0.1135geomean0.1296
-
Lua 5.4.7
nbody0.0142splay0.4393richards0.0217deltablue0.0832geomean0.0577
-
QuickJS-ng 0.14.0
nbody0.0214splay0.7090richards0.7193deltablue0.1585geomean0.2036
-
Zef Başlangıç Sürümü
nbody2.9573splay13.0286richards1.9251deltablue5.9997geomean4.5927
-
Zef Değişiklik #1: Doğrudan Operatörler
nbody2.1891splay12.0233richards1.6935deltablue5.2331geomean3.9076
-
Zef Değişiklik #2: Doğrudan RMW'ler
nbody2.0130splay11.9987richards1.6367deltablue5.0994geomean3.7677
-
Zef Değişiklik #3: IntObject'ten Kaçınma
nbody1.9922splay11.8824richards1.6220deltablue5.0646geomean3.7339
-
Zef Değişiklik #4: Semboller
nbody1.5782splay9.9577richards1.4116deltablue4.4593geomean3.1533
-
Zef Değişiklik #5: Satır İçi Değer
nbody1.4982splay9.7723richards1.3890deltablue4.3536geomean3.0671
-
Zef Değişiklik #6: Nesne Modeli ve Satır İçi Önbellekler
nbody0.3884splay3.3609richards0.2321deltablue0.6805geomean0.6736
-
Zef Değişiklik #7: Argümanlar
nbody0.3160splay2.6890richards0.1653deltablue0.4738geomean0.5077
-
Zef Değişiklik #8: Getter'lar
nbody0.2988splay2.6919richards0.1564deltablue0.4260geomean0.4809
-
Zef Değişiklik #9: Setter'lar
nbody0.2850splay2.6690richards0.1514deltablue0.4072geomean0.4651
-
Zef Değişiklik #10: Satır içi callMethod
nbody0.2533splay2.6711richards0.1513deltablue0.4032geomean0.4506
-
Zef Değişiklik #11: Hashtable
nbody0.1796splay2.6528richards0.1379deltablue0.3551geomean0.3906
-
Zef Değişiklik #12: std::optional'den Kaçınma
nbody0.1689splay2.6563richards0.1379deltablue0.3518geomean0.3839
-
Zef Değişiklik #13: Özelleştirilmiş Argümanlar
nbody0.1610splay2.5823richards0.1350deltablue0.3372geomean0.3707
-
Zef Değişiklik #14: İyileştirilmiş Value Slow Path'leri
nbody0.1348splay2.5062richards0.1241deltablue0.3076geomean0.3367
-
Zef Değişiklik #15: Tekilleştirilmiş DotSetRMW::evaluate
nbody0.1342splay2.5047richards0.1256deltablue0.3079geomean0.3375
-
Zef Değişiklik #16: Hızlı sqrt
nbody0.1274splay2.5045richards0.1251deltablue0.3060geomean0.3322
-
Zef Değişiklik #17: Hızlı toString
nbody0.1282splay2.2664richards0.1275deltablue0.2964geomean0.3235
-
Zef Değişiklik #18: Dizi Literal Özelleştirmesi
nbody0.1295splay1.6661richards0.1250deltablue0.2979geomean0.2992
-
Zef Değişiklik #19: Value callOperator Optimizasyonu
nbody0.1208splay1.6698richards0.1143deltablue0.2713geomean0.2810
-
Zef Değişiklik #20: Daha İyi C++ Yapılandırması
nbody0.1186splay1.6521richards0.1127deltablue0.2635geomean0.2760
-
Zef Değişiklik #21: Assert Yok
nbody0.1194splay1.6504richards0.1127deltablue0.2619geomean0.2759
-
Yolo-C++ içinde Zef
nbody0.0233splay0.3992richards0.0309deltablue0.0784geomean0.0686
1 yorum
Hacker News yorumları
Benzer bir bağlamda, Wren yorumlayıcısının performansını ele alan şu sayfa epey ilginçti
Zef yazısı uygulama tekniklerine odaklanıyorsa, Wren tarafı da dil tasarımının performansa nasıl katkı verdiğini gösteriyor gibi hissettirdi
Özellikle Wren’in dynamic object shapes yaklaşımından vazgeçerek copy-down inheritance’ı mümkün kılması ve method lookup’u çok daha basit hale getirmesi iyi göründü
Bana göre bu gayet makul bir trade-off. Bir sınıf oluşturulduktan sonra ona sonradan metod eklemek pratikte ne kadar sık gereken bir şey, emin değilim
Dinamik diller için çok optimize edilmiş pek çok VM var, ama LuaJIT’in güçlü olmasının sebebi bence Lua’nın zaten çok küçük ve optimizasyona çok uygun bir dil olması
Optimize etmesi zor bazı özellikler elbette var, ama sayıları az olduğu için uğraşmaya değer kalıyor
Python ise bana tamamen farklı geliyor. Biraz abartıyla, hızlı bir JIT ihtimalini en aza indirecek şekilde tasarlanmış gibi; üst üste binen dinamiklik katmanları optimizasyonu gerçekten zorlaştırıyor
Bunca yıllık çalışmadan sonra bile CPython 3.15 JIT’in x86_64’te varsayılan yorumlayıcıdan yalnızca yaklaşık %5 daha hızlı olması da bunu iyi gösteriyor gibi
Tabii Ruby’nin hız öncelikli bir dil olarak bilinmediği de akla geliyor
Öte yandan, bir tipin uygulanabilir fonksiyon kümesini kapalı biçimde taşıması fikri bana biraz kuşkulu da geliyor
Dünyada, rastgele fonksiyonları tanımlayıp ilk argüman tipi uyan değerlere nokta gösterimiyle metod gibi ekleyerek kullanabildiğiniz epey dil var
Örneğin Nim’in makroları, Scala’nın implicit classes ve type classes yapıları, Kotlin’in extension functions’ları, Rust’ın traits yaklaşımı gibi
Karmaşık dinamik diller bunu mümkün kılan zemini çeşitli şekillerde aktif olarak bozduğu için optimizasyon daha zor hale geliyor
Geriye dönüp bakınca bu aslında oldukça bariz bir şey gibi duruyor
Değişiklik #5’ten #6’ya geçerken inline caches ile hidden-class object model’in performans artışının büyük kısmını sağlaması, tarihsel olarak V8 ya da JSC’nin hızlanma biçimine gerçekten çok benziyordu
Naif yorumlayıcının duvara tosladığı nokta sonuçta property access sırasında yapılan dinamik dispatch; geri kalan her şey görece bir rounding error gibi görünüyor
Her adımın ne kadar katkı yaptığını ayrı ayrı gösterecek şekilde sunmaları da hoştu. Performans yazıları çoğu zaman sadece son rakamı atıp geçiyor
Bytecode yorumlayıcılarında bytecode akışındaki sabit ofsetleri patch edebildiğiniz için IC yeniden yazım noktası doğaldır
Ama burada önbellek konumu AST düğümü olduğundan, @pizlonator’un
constructCache<>ile generic düğümün üstüne specialized AST düğümünü in-place yerleştirmesi etkileyiciydiSonuçta bu, AST seviyesinde self-modifying code gibi görünüyordu
Buna karşılık bu yaklaşım mutable AST nodes gerektiriyor; bu da alt ağaç paylaşımı ya da paralel derleme gibi birçok derleyicinin beklediği immutable AST varsayımıyla çakışıyor
Tek iş parçacıklı bir yorumlayıcı için temiz bir çözüm, ama aynı AST arka plandaki bir iş parçacığında JIT derlenirken yorumlayıcının düğümleri değiştirmesi sorun yaratabilir gibi
Bence bu, gerçek üretim kodunun çoğunu iyi temsil etmiyor olabilir
Böyle hissetmemin nedeni sqrt optimizasyonunun %1,6 iyileşme sağlamasıydı
Böyle bir iyileşme çıkması için benchmark süresinin en az %1,6’sının zaten orada harcanıyor olması gerekir; bu da beni epey şaşırttı
Git reposuna bakınca bunun gerçekten nbody simülasyonunda yaşandığı anlaşılıyor gibiydi
Ben de yakın zamanda kendi ilk AST-walking interpreter sürümümü yayımladığım için bunu daha da ilgiyle okudum
Benim hedefim, yorumlanan bir dil yapmak için taban seviyede ne gerektiğini anlamaktı
Optimizasyon karmaşıklığını eklemek istemedim; sadece kendi Rust kodumu kendim anlayabilecek hale getirmeye odaklandım
Ama sadece sevdiğim dil olan Rust’ı kullanmış olmam bile performansın epey iyi çıkmasına yettiğine şaşırdım
Üstelik Rust ownership ve lifetime işini üstlendiği için ayrıca bir garbage collector gerekmemesi de bonus oldu
Elbette şu anda closure gibi alanlarda lifetime cehenneminden kaçınmak için clone’a oldukça muhafazakâr biçimde yaslanıyorum, ama buna rağmen hız ve bellek profili gayet yeterli geliyor
Basit ve anlaşılması kolay, Rust tabanlı bir tree-walking interpreter ile ilgileniyorsanız benim yorumlayıcım gluonscript’e bakabilirsiniz
Yazı gerçekten çok iyiydi
Özellikle Arguments arkı, yani #7’den #13’e giden akış, benim deneyimimle çok örtüştü
Daha önce Rust ile async bir step evaluator yazarken, normalde borrow etmenin avantaj sağlayacağına inanıp
Cow<'_, Input>içine epey dalmıştımMikrobenchmarklarda iyi görünüyordu, ama gerçek iş yükünde Cow’un discriminant’ı ve lifetime kaynaklı karmaşıklık ilk
awaitsonrasında tüm combinator’lara yayıldı; inlining ciddi şekilde bozulunca Cow kullanma gerekçesi de ortadan kalktıSonunda evaluator sınırında
NoInput / OneInput / MultiInput(Vec)modeline geçtim; görünüşte kaba dursa da sonuçta buradaki ZeroArguments / OneArgument / TwoArguments ayrımına neredeyse aynı noktadan varmış oldumHâlâ merak ettiğim bir şey, native yol üzerinde arity specialization’ın üstüne type specialization da eklenip eklenmediği
Mesela binary tarzı bir yaklaşımda
isIntkontrolünün kendisini bile ortadan kaldırmak mümkün olabilir gibiTahminim ya kod boyutu hesabı buna uymadı ya da nesne tarafında IC zaten sıcak yolları yeterince kapsadığı için native fast path’in etkisi büyük olmadı
Hangisi olduğunu merak ettim
Bu gerçekten ilginç ve iyi yapılmış bir çalışmaydı
Ben de benzer bir şey yaptım ama daha çok fonksiyonel tarafa yakın bir dil olan Scheme üzerindeydi
Burada en büyük kazancı nesne optimizasyonları sağlamış, ama benim durumumda asıl belirleyici olan closures optimizasyonuydu
İlginç olan, optimizasyon yöntemlerinin kendisinin oldukça benzer olması
Scheme’i yeterince hızlı yapmanın cevabı neredeyse tamamen Three implementation models for scheme içinde var diye düşünüyorum
Ama orada belli ölçüde bir derleme aşaması olduğu için model doğrudan özgün AST’yi yorumlama yaklaşımı değil
İlginçti, paylaştığın için teşekkürler
Ben de bir gün bu konuyu derinlemesine incelemek isterim diye düşündüm
Ayrıca GitHub’a göre reponun %99,7 HTML ve %0,3 C++ olması da oldukça komik ve akılda kalıcıydı
Bu da yorumlayıcının gerçekten çok küçük olduğunun kanıtı gibi geldi
Tarayıcı için kod üretme şekli yüzünden site tarafı gereksiz yere büyümüş
Yine de yorumlayıcının kendisi gerçekten çok küçük
Bunu yaparken fil c’nin kendisini daha iyi hale getirecek şeyler öğrenip öğrenmediğini merak ettim
Ayrıca value object metodlarını outline call olarak işlemenin maliyetinin de epey yüksek olduğunu öğrendim
Lua’nın dahil edildiğini gördüm, ama keşke LuaJIT de olsaydı diye düşündüm
Hatta o kadar mühendislik emeği düşünülünce zaten öyle olması gerekir diye beklerim
Dahil edilebilecek çok sayıda çalışma zamanı vardı ama hepsini eklemedim
Ayrıca PUC Lua’nın QuickJS ya da Python’dan epey hızlı olması da oldukça etkileyiciydi
Fil-C’yi gerçekten kullanma deneyiminin nasıl olduğunu, pratikte gerçekten faydalı olup olmadığını merak ettim
Yine de bu projede oldukça somut bir fayda sağladı
Nesne modelinin tasarımını, aksi halde olacağından çok daha kolay hale getiren birçok bellek güvenliği sorununu deterministik biçimde yakaladı
Ayrıca exact GC eklenmiş C++ bana gerçekten çok iyi bir programlama modeli gibi geldi
Düz C++’a kıyasla üretkenliğimin yaklaşık 1,5 kat arttığını hissettim; diğer GC dilleriyle karşılaştırınca bile geliştirme hızım sanki 1,2 kat daha iyiydi
Bunun nedeni bence C++’ın API ekosisteminin zengin olması ve lambdas, templates, class system gibi özelliklerinin çok olgunlaşmış olması
Elbette birçok açıdan taraflı olduğumu da kabul ediyorum
Sonuçta Fil-C++’ı ben yaptım ve C++’ı da yaklaşık 35 yıldır kullanıyorum
Yazıda geçen YOLO-C/C++ derleyicisinin ne olduğunu merak ettim
Aratınca pek bir şey çıkmıyor, chatgpt de bilmiyor gibi görünüyordu