6 puan yazan GN⁺ 8 일 전 | 1 yorum | WhatsApp'ta paylaş
  • 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 sqrt ve toString ö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*
    • double değerler 0x1000000000000 offset’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
    • 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
  • 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 unsafe koda izin vermek halinde, kısmen ya da tamamen Rust kullanma ihtimalinden bahsediliyor
  • 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::evaluate metod yapısı
    • Aşırı string kullanımı
      • Get AST düğümü, değişken adını açıklayan bir std::string saklı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 anahtarla std::unordered_map araması 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
  • 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 DotCall düğümleri olarak üretmek yerine, her operatör için ayrı AST düğümleri oluşturuyor
    • Zef'te a + b ile a.add(b) aynıdır
      • Önceden a + b, DotCall(a, "add") ve argüman b olarak parse ediliyordu
      • Her aritmetik işlemde operatör metot adı dizgesine bakma maliyeti oluşuyordu
      • DotCall, dizgeyi Value::callMethod'a iletiyordu
      • Value::callMethod, çoklu dizge karşılaştırmaları yapıyordu
    • Değişiklikten sonra parser Binary<> ve Unary<> düğümleri oluşturuyor
      • Template ve lambda'lar kullanılarak her operatör için farklı Node::evaluate override'ları sağlanıyor
      • Her düğüm, ilgili operatörün Value hızlı yolunu doğrudan çağırıyor
      • Örneğin a + b, Binary<add için lambda>::evaluate çağrısının ardından Value::add çağrısını yapıyor
    • 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ş
  • Optimizasyon #2: RMW operatörlerini doğrudan çağırma

    • Genel operatörler hızlandı, ancak a += b gibi 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 makeRMW sanal ç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, id değişken okumasına karşılık geliyor
      • Dot, expr.id için kullanılıyor
      • Subscript, expr[index] için kullanılıyor
    • Her sanal çağrı SPECIALIZE_NEW_RMW makrosunu kullanıyor
      • SetRMW, id += value
      • DotSetRMW, expr.id += value
      • SubscriptRMW, expr[index] += value
    • Değişiklik #1'deki operatör özelleştirmesi lambda dispatch kullanıyordu
    • RMW ise enum kullanıyor
      • Bunun nedeni get, dot, subscript olmak ü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
    • 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ı
  • Optimizasyon #3: IntObject kontrolünden kaçınma

    • Darboğaz, Value hızlı yolunun isInt() kullanması ve bunun içindeki isIntSlow()'un Object::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 Value içinde tutmaktı
    • Optimizasyondan sonra Value hı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ı
    • 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ı
  • 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::callMethod ailesiydi
    • 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 Symbol nesne işaretçileriyle değiştirdi
    • Yeni bir Symbol sınıfı eklendi
      • Uygulama symbol.h ve symbol.cpp iç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
    • Dizge literal'leri yerine önceden hazırlanmış semboller kullanılıyor
      • Örneğin "subscript" yerine Symbol::subscript
    • Çok sayıda fonksiyon imzası const std::string& yerine Symbol* 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ı
  • 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.h yer alıyor
    • Bunun value.h yerine 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 Context nesnesi ayrılıyordu
      • Her Context, o scope’taki değişkenleri tutan bir hash tablosu barındırıyordu
    • Nesneler daha karmaşık bir yapıdaydı
      • Her nesne, örneği olduğu sınıfları Contexte eşleyen bir hash tablosu tutuyordu
    • Bu yapının gerekli olmasının nedeni kalıtım ve iç içe scope’lar
      • Bar, Foodan kalıtım aldığında Bar ve Foo farklı scope’ları closure olarak yakalayabiliyor
      • Ayrıca aynı ada sahip farklı private alanlara da sahip olabiliyorlar
    • Yeni yapı Storage kavramını tanıtıyor
      • Veri, offset’lere göre depolanıyor
      • offset ise hangi Context tarafından belirlendiğine göre tanımlanıyor
    • Context hâlâ varlığını koruyor ancak nesne ya da scope oluşturulurken değil, AST’nin resolve geç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
  • Inline cache

    • expr.name gibi bir kod konumunda, en son görülen exprin dinamik türünü ve namein çö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, Package genelinde CacheRecipe çağrıları eklenmiş
      • Erişim sürecine dair bilgi toplanıyor
    • Dot::evaluate gibi AST değerlendirme fonksiyonları, yürüttükleri çok biçimli işlemlerden elde ettikleri CacheRecipe’yi this ile birlikte constructCache<> fonksiyonuna iletiyor
    • constructCache
      • CacheRecipeye 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 cache nesnesiyle hızlı çağrı deneniyor
      • cache nesnesinin türü constructCache<> tarafından belirleniyor
  • Watchpoint

    • Leksiksel scope’ta x değişkeni bulunduğu ve bunun içinde Foo sınıfı yer aldığı, Foo metodunun da xe eriştiği bir örnek veriliyor
    • Foo içinde x adında bir fonksiyon ya da değişken yoksa doğrudan dıştaki x okunabiliyormuş gibi görünüyor
    • Ancak bir alt sınıf x adında bir getter ekleyebilir
    • Bu durumda erişimin sonucu dıştaki x değil, getter olmalıdır
    • Inline cache, böyle bir değişiklik olasılığını ele almak için çalışma zamanında Watchpoint kuruyor
    • Örnekte, bu adın override edilip edilmediğini izleyen bir watchpoint kullanılıyor
  • Üç ö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 CacheRecipe sü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::tryCallMethod sanal çağrısını yakalayarak uyguluyordu
      • Yeni nesne modelinde Object iç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 Array metodlarını sunmak için bu metodları barındıran Array’e özel sınıfın kendisini oluşturmak gerekiyor
    • Sonuç olarak intrinsic işlevlerin önemli bir bölümü, uygulamanın geneline dağılmış yapıdan çıkıp makerootcontext.cpp merkezli 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

Ç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
    • optional gerekmesinin nedeni, bazı uç durumlarda şu ikisini ayırmak zorunda olmasıydı:
      • o.getter
      • o.function()
    • Zef'te çoğu durumda ikisi de fonksiyon çağrısıdır, ancak istisna olarak şu kod vardır:
      • o.NestedClass
      • o.NestedClass()
    • İlki NestedClass nesnesinin 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 vector allocate ediyor
      • Çağrılan taraf da bu vektörü kopyalayan bir arguments scope yeniden allocate ediyordu
    • Değişiklik olarak Arguments tipi 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 malloc kaldırılarak allocation sayısı azaltıldı
    • Fil-C++'ta ise std::optional'ın kendisi heap allocation yapıyor
      • std::optional olmasa bile const 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ı
  • 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 f içine kaydeder
    • Aynı türün instance'ları bile başka nesnenin f alanına erişemez
      • Örnek: fn nope(o) o.f
      • println(Foo(42).nope(Foo(666)))
      • nope içindeki o.f, o'nun f alanına erişemez
    • 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ğil f adlı bir metodu çağırma isteğidir
    • Bu yüzden şu kalıp sık görülür
      • my f
      • fn f f
      • yani yerel değişken f'yi döndüren, adı f olan bir metot
    • Daha kısa sözdizimi olarak readable f vardır
      • my f ve fn f f iç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 UserFunction vardır
      • Yeni Node::inferGetter metodu ile fonksiyon gövdesinin basit bir getter olup olmadığı çıkarımlanır
    • Çıkarım kuralları
      • Block::inferGetter, içerdiği her şey getter olarak çıkarımlanabiliyorsa kendisi de getter olarak çıkarımlanır
      • Get::inferGetter, kendisini getter olarak çıkarımlar ve yüklenecek offset değerini döndürür
      • Context::tryGetFieldOffsets, getter'ın çalışacağı lexical scope içinde o field'ın kesin olarak var olduğu durumda yalnızca boş olmayan bir Offsets döndürür
      • UserFunction, fonksiyon gövdesi getter olarak çıkarımlanabiliyorsa, bilinen offset'ten doğrudan okuma yapan özel bir Function alt 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 = newValue kalıbını eşleştirmek gerekir
    • UserFunction'ın çıkarım aşamasında setter'ın parametre adı aktarılmalıdır
    • Set'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ı
  • Optimizasyon #10: callMethod inline 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::tryCallMethod ve ClassObject::TryCallMethodDirect yolları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.h içinde, tüm tryCallMethodSlow yoluna inmeden önce önce bu global tablo sorgulanıyor
      • classobject.cpp iç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ı
  • Optimizasyon #12: std::optional'dan kaçınma

    • Fil-C++'ta union ile ilgili derleyici patolojileri nedeniyle std::optional heap 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 FilPizlonator geç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::optional gibi 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ş
  • 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 Arguments nesnesi 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 Arguments nesnesi 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 Arguments nesnesi tahsisinden kaçınabilir
    • ZeroArguments, (Arguments*)nullptr ile 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
    • 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::getArg kullanı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ı

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 Value için out-of-line slow path, Value'nin bir üye fonksiyonuydu ve örtük bir const 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
    • Değişiklikten sonra bu metotlar static yapıldı ve Value, 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: DotSetRMW yinelemelerini 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ın int veya double olup olmadığını kontrol etmeye dayanıyor
    • Bu yaklaşım yalnızca parser'ın tanıdığı operatörler için geçerli
      • value.sqrt gibi biçimlerde uygulanamıyor
    • Bu değişiklikle Dot, value.sqrt iç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ı
  • 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, 3 her seferinde yeniden özyinelemeli olarak değerlendiriliyordu
    • Bu değişiklik, ArrayLiteral düğü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::callOperator iyileştirmesi

    • Daha önce Value'yu referansla geçirmeyerek hız kazanımı sağlayan optimizasyonun aynısı, callOperator slow 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ı
  • 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 ZASSERT makrosunu kullanıyordu
      • Bu yapı assert'leri her zaman çalıştırıyordu
    • Değişiklikten sonra dahili ASSERT makrosu kullanıldı
      • Assert'ler yalnızca ASSERTS_ENABLED ayarlıysa çalışıyor
    • 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
  • 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, geomean yer alır
  • Python 3.10

    • nbody 0.0364
    • splay 0.8326
    • richards 0.0822
    • deltablue 0.1135
    • geomean 0.1296
  • Lua 5.4.7

    • nbody 0.0142
    • splay 0.4393
    • richards 0.0217
    • deltablue 0.0832
    • geomean 0.0577
  • QuickJS-ng 0.14.0

    • nbody 0.0214
    • splay 0.7090
    • richards 0.7193
    • deltablue 0.1585
    • geomean 0.2036
  • Zef Başlangıç Sürümü

    • nbody 2.9573
    • splay 13.0286
    • richards 1.9251
    • deltablue 5.9997
    • geomean 4.5927
  • Zef Değişiklik #1: Doğrudan Operatörler

    • nbody 2.1891
    • splay 12.0233
    • richards 1.6935
    • deltablue 5.2331
    • geomean 3.9076
  • Zef Değişiklik #2: Doğrudan RMW'ler

    • nbody 2.0130
    • splay 11.9987
    • richards 1.6367
    • deltablue 5.0994
    • geomean 3.7677
  • Zef Değişiklik #3: IntObject'ten Kaçınma

    • nbody 1.9922
    • splay 11.8824
    • richards 1.6220
    • deltablue 5.0646
    • geomean 3.7339
  • Zef Değişiklik #4: Semboller

    • nbody 1.5782
    • splay 9.9577
    • richards 1.4116
    • deltablue 4.4593
    • geomean 3.1533
  • Zef Değişiklik #5: Satır İçi Değer

    • nbody 1.4982
    • splay 9.7723
    • richards 1.3890
    • deltablue 4.3536
    • geomean 3.0671
  • Zef Değişiklik #6: Nesne Modeli ve Satır İçi Önbellekler

    • nbody 0.3884
    • splay 3.3609
    • richards 0.2321
    • deltablue 0.6805
    • geomean 0.6736
  • Zef Değişiklik #7: Argümanlar

    • nbody 0.3160
    • splay 2.6890
    • richards 0.1653
    • deltablue 0.4738
    • geomean 0.5077
  • Zef Değişiklik #8: Getter'lar

    • nbody 0.2988
    • splay 2.6919
    • richards 0.1564
    • deltablue 0.4260
    • geomean 0.4809
  • Zef Değişiklik #9: Setter'lar

    • nbody 0.2850
    • splay 2.6690
    • richards 0.1514
    • deltablue 0.4072
    • geomean 0.4651
  • Zef Değişiklik #10: Satır içi callMethod

    • nbody 0.2533
    • splay 2.6711
    • richards 0.1513
    • deltablue 0.4032
    • geomean 0.4506
  • Zef Değişiklik #11: Hashtable

    • nbody 0.1796
    • splay 2.6528
    • richards 0.1379
    • deltablue 0.3551
    • geomean 0.3906
  • Zef Değişiklik #12: std::optional'den Kaçınma

    • nbody 0.1689
    • splay 2.6563
    • richards 0.1379
    • deltablue 0.3518
    • geomean 0.3839
  • Zef Değişiklik #13: Özelleştirilmiş Argümanlar

    • nbody 0.1610
    • splay 2.5823
    • richards 0.1350
    • deltablue 0.3372
    • geomean 0.3707
  • Zef Değişiklik #14: İyileştirilmiş Value Slow Path'leri

    • nbody 0.1348
    • splay 2.5062
    • richards 0.1241
    • deltablue 0.3076
    • geomean 0.3367
  • Zef Değişiklik #15: Tekilleştirilmiş DotSetRMW::evaluate

    • nbody 0.1342
    • splay 2.5047
    • richards 0.1256
    • deltablue 0.3079
    • geomean 0.3375
  • Zef Değişiklik #16: Hızlı sqrt

    • nbody 0.1274
    • splay 2.5045
    • richards 0.1251
    • deltablue 0.3060
    • geomean 0.3322
  • Zef Değişiklik #17: Hızlı toString

    • nbody 0.1282
    • splay 2.2664
    • richards 0.1275
    • deltablue 0.2964
    • geomean 0.3235
  • Zef Değişiklik #18: Dizi Literal Özelleştirmesi

    • nbody 0.1295
    • splay 1.6661
    • richards 0.1250
    • deltablue 0.2979
    • geomean 0.2992
  • Zef Değişiklik #19: Value callOperator Optimizasyonu

    • nbody 0.1208
    • splay 1.6698
    • richards 0.1143
    • deltablue 0.2713
    • geomean 0.2810
  • Zef Değişiklik #20: Daha İyi C++ Yapılandırması

    • nbody 0.1186
    • splay 1.6521
    • richards 0.1127
    • deltablue 0.2635
    • geomean 0.2760
  • Zef Değişiklik #21: Assert Yok

    • nbody 0.1194
    • splay 1.6504
    • richards 0.1127
    • deltablue 0.2619
    • geomean 0.2759
  • Yolo-C++ içinde Zef

    • nbody 0.0233
    • splay 0.3992
    • richards 0.0309
    • deltablue 0.0784
    • geomean 0.0686

1 yorum

 
GN⁺ 8 일 전
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

    • Yorumlayıcı ya da JIT hızını dil tasarımının çok büyük ölçüde belirlediğini düşünüyorum
      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
    • Bu yaklaşım, monkey patching’in deyimsel olarak kabul edildiği dillerde, özellikle Ruby’de sürekli yapılan şeye benziyor gibi geliyor
      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
    • Benim deneyimime göre, genel olarak bir ifadeye statik tip atayabiliyorsanız onu oldukça verimli derleyebilirsiniz
      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

    • #6’daki özellikle ilginç uygulama detayı, AST’yi doğrudan gezen bir yorumlayıcıda inline caching’in nasıl yapıldığıydı
      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 etkileyiciydi
      Sonuç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
    • Genel yönelime katılıyorum ama bunun sonuçta yalnızca belirli bir benchmark için elde edilmiş bir sonuç olduğuna dair küçük bir not düşmek gerekir diye düşünüyorum
      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ım
    Mikrobenchmarklarda iyi görünüyordu, ama gerçek iş yükünde Cow’un discriminant’ı ve lifetime kaynaklı karmaşıklık ilk await sonrası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ış oldum
    Hâ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 isInt kontrolünün kendisini bile ortadan kaldırmak mümkün olabilir gibi
    Tahminim 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

    • Bunun sebebi statik üretilmiş siteyi de commit etmiş olmaları
      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

    • Kesinlikle unions ele alışımız için daha iyi bir çözüme ihtiyaç olduğunu gördüm
      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

    • Tahminim LuaJIT’in Zef’i ezip geçeceği yönünde
      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

    • Fil’in kendisi olduğum için önyargılı olduğumu baştan söyleyeyim
      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

    • Fil-C’nin yazarı ve bu dilin de yaratıcısı, Yolo-C/C++ ifadesini Fil-C olmayan sıradan C/C++’ı kastetmek için kullanmış