1 poin oleh GN⁺ 2 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Bahkan interpreter yang menelusuri AST secara langsung juga bisa memperoleh peningkatan performa besar hanya dengan representasi nilai, inline cache, model objek, watchpoint, dan optimasi detail yang diulang-ulang
  • Baseline Zef yang nyaris tidak mempertimbangkan performa 35 kali lebih lambat daripada CPython 3.10, 80 kali lebih lambat daripada Lua 5.4.7, dan 23 kali lebih lambat daripada QuickJS-ng 0.14.0, tetapi setelah 21 tahap optimasi berhasil mencapai akselerasi 16,646 kali
  • Lompatan terbesar muncul dari desain ulang model objek yang dikombinasikan dengan inline cache, lalu peningkatan 4,55 kali berlanjut lewat akses berbasis Storage dan Offsets, spesialisasi AST yang di-cache, serta penerapan watchpoint untuk memantau override nama
  • Perbaikan tambahan mencakup menghapus dispatch berbasis string, memperkenalkan Symbol, mengubah struktur pengiriman argumen, spesialisasi getter dan setter, jalur singkat hash table, hingga spesialisasi array literal dan sqrt·toString yang diterapkan secara kumulatif
  • Jika termasuk port Yolo-C++, hasilnya tercatat 66,962 kali lebih cepat dibanding baseline, 1,889 kali lebih cepat daripada CPython 3.10 dan 2,968 kali lebih cepat daripada QuickJS-ng 0.14.0, tetapi tidak cocok untuk workload jangka panjang karena tidak ada pembebasan memori

Pendahuluan dan metodologi evaluasi

  • Target optimasinya adalah interpreter yang menelusuri AST secara langsung, dengan tujuan mendorong bahasa dinamis buatan iseng bernama Zef hingga mampu bersaing dengan Lua, QuickJS, dan CPython
    • Alih-alih penyetelan halus pada compiler JIT atau GC yang matang, fokusnya adalah pada optimasi yang bisa diterapkan bahkan dari titik awal tanpa fondasi
    • Teknik yang dibahas adalah representasi nilai, inline caching, model objek, watchpoint, dan penerapan berulang optimasi yang masuk akal
  • Hanya dengan teknik di isi utama, berhasil dicapai peningkatan performa besar bahkan tanpa SSA, GC, bytecode, maupun machine code
    • Berdasarkan isi utama, akselerasi 16 kali
    • Jika termasuk port Yolo-C++ yang belum selesai, akselerasi 67 kali
  • Evaluasi performa menggunakan suite benchmark ScriptBench1
    • Benchmark yang disertakan adalah scheduler OS Richards, constraint solver DeltaBlue, simulasi fisika N-Body, dan uji binary tree Splay
    • Menggunakan port yang sudah ada untuk JavaScript, Python, dan Lua
    • Port Python dan Lua untuk Splay dibuat dengan Claude
  • Lingkungan eksperimen adalah Ubuntu 22.04.5, Intel Core Ultra 5 135U, RAM 32GB, Fil-C++ 0.677
    • Lua 5.4.7 dikompilasi dengan GCC 11.4.0
    • QuickJS-ng 0.14.0 menggunakan biner dari GitHub releases
    • CPython 3.10 menggunakan versi bawaan Ubuntu
  • Semua eksperimen menggunakan nilai rata-rata dari 30 kali eksekusi yang diacak
  • Sebagian besar perbandingan dilakukan antara interpreter Zef yang dikompilasi dengan Fil-C++ dan interpreter lain yang dibangun dengan compiler Yolo-C

Interpreter Zef asli

  • Disebutkan bahwa interpreter ini ditulis hampir tanpa mempertimbangkan performa, dan hanya ada dua pilihan yang dibuat dengan kesadaran performa
  • Representasi nilai

    • Menggunakan tagged value 64-bit
      • Nilai yang dapat ditampung adalah double, integer 32-bit, dan Object*
    • double direpresentasikan dengan metode offset 0x1000000000000
      • Diperkenalkan sebagai teknik yang dipelajari dari JavaScriptCore
      • Dalam literatur disebut NuN tagging
    • Integer dan pointer menggunakan representasi native
      • Bergantung pada asumsi bahwa nilai pointer tidak lebih kecil dari 0x100000000
      • Secara langsung dinyatakan sebagai pilihan yang berbahaya
      • Disebutkan bahwa sebagai alternatif, integer bisa diberi tag bit atas 0xffff000000000000
    • Dengan representasi ini, pada operasi numerik dimungkinkan penerapan jalur cepat berbasis bit test
    • Manfaat yang lebih penting adalah menghindari alokasi heap untuk angka
    • Saat membuat interpreter baru, penting untuk memilih representasi nilai dasar dengan benar sejak awal, karena mengubahnya nanti sangat sulit
    • Sebagai titik awal implementasi bahasa bertipe dinamis, diajukan tagged value 32-bit atau 64-bit
  • Pemilihan bahasa implementasi

    • Memilih keluarga C++ sebagai bahasa yang mampu menampung optimasi secara memadai
    • Disebutkan bahwa Java tidak akan dipilih karena batas atas optimasi level rendah
    • Dijelaskan bahwa Rust juga tidak akan dipilih karena representasi heap untuk implementasi bahasa GC memerlukan status global yang dapat berubah dan referensi siklik
      • Disebutkan bahwa sebagian atau seluruh implementasi masih mungkin memakai Rust jika menerima konfigurasi multi-bahasa atau mengizinkan banyak kode unsafe
  • Pilihan yang keliru dari sudut pandang rekayasa performa

    • Menggunakan Fil-C++
      • Memungkinkan pengembangan cepat dan memberikan GC secara gratis
      • Melaporkan pelanggaran keamanan memori dengan informasi diagnostik dan stack trace
      • Tidak ada undefined behavior
      • Biaya performanya biasanya sekitar 4 kali
    • Interpreter AST walking rekursif
      • Menggunakan struktur metode virtual Node::evaluate yang dioverride di banyak tempat
    • Penyalahgunaan string
      • Node AST Get menyimpan std::string yang menjelaskan nama variabel
      • String itu digunakan setiap kali variabel diakses
    • Penyalahgunaan hash table
      • Saat Get dijalankan, dilakukan lookup std::unordered_map dengan key string
    • Pencarian scope berbasis rantai pemanggilan rekursif
      • Mengizinkan hampir semua bentuk nested structure dan closure
      • Dalam nested seperti kelas A di dalam fungsi F, dan fungsi G di dalam kelas B, metode A dapat melihat field A, variabel lokal F, field B, variabel lokal G
      • Implementasi awal menangani ini dengan fungsi rekursif C++ yang mengkueri objek scope yang berbeda-beda
  • Karakteristik implementasi awal

    • Meski ada pilihan-pilihan yang keliru, tetap dimungkinkan membuat interpreter bahasa yang cukup kompleks dengan sedikit kode
    • Modul terbesar adalah parser
    • Sisanya cenderung sederhana dan jelas
  • Performa awal

    • Interpreter asli 35 kali lebih lambat daripada CPython 3.10
    • 80 kali lebih lambat daripada Lua 5.4.7

      • 23 kali lebih lambat daripada QuickJS-ng 0.14.0

Tabel perkembangan optimasi keseluruhan

  • Tabel tersebut merangkum perubahan performa dari Zef Baseline hingga Zef Change #21: No Asserts, serta Zef in Yolo-C++
    • Kolom pembanding adalah vs Zef Baseline, vs Python 3.10, vs Lua 5.4.7, vs QuickJS-ng 0.14.0
  • Berdasarkan baris terakhir, Zef Change #21: No Asserts 16,646 kali lebih cepat dibanding baseline
    • 2,13 kali lebih lambat daripada Python 3.10

    • 4,781 kali lebih lambat daripada Lua 5.4.7

      • 1,355 kali lebih lambat daripada QuickJS-ng 0.14.0
  • Zef in Yolo-C++** 66,962 kali lebih cepat dibanding baseline**

    • 1,889 kali lebih cepat daripada Python 3.10

    • 1,189 kali lebih lambat daripada Lua 5.4.7

      • 2,968 kali lebih cepat daripada QuickJS-ng 0.14.0

Tahap optimasi awal

  • Optimasi #1: pemanggilan operator langsung

    • Parser tidak lagi membuat operator sebagai node DotCall dengan nama operator, melainkan membuat node AST terpisah untuk setiap operator
    • Di Zef, a + b dan a.add(b) itu sama
      • Awalnya, a + b di-parse sebagai DotCall(a, "add") dengan argumen b
      • Pada setiap operasi aritmetika, terjadi pencarian string nama metode operator
      • DotCall meneruskan string ke Value::callMethod
      • Value::callMethod melakukan beberapa perbandingan string
    • Setelah diubah, parser membuat node Binary<> dan Unary<>
      • Dengan memanfaatkan template dan lambda, disediakan override Node::evaluate yang berbeda untuk tiap operator
      • Setiap node langsung memanggil jalur cepat Value untuk operator tersebut
      • Contohnya, a + b memanggil Binary<lambda for add>::evaluate lalu Value::add
    • Dampak performanya adalah peningkatan 17,5%
      • Pada titik ini, performanya 30 kali lebih lambat dari CPython 3.10
      • 67 kali lebih lambat dari Lua 5.4.7
      • 19 kali lebih lambat dari QuickJS-ng 0.14.0
  • Optimasi #2: pemanggilan operator RMW langsung

    • Operator biasa sudah lebih cepat, tetapi bentuk RMW seperti a += b masih memakai dispatch berbasis string
    • Parser diubah agar membuat node terpisah untuk tiap kasus RMW
    • Parser meminta agar node LValue mengganti dirinya menjadi RMW melalui pemanggilan virtual makeRMW
    • LValue yang diubah menjadi RMW adalah Get, Dot, dan Subscript
      • Get merepresentasikan pembacaan variabel id
      • Dot merepresentasikan expr.id
      • Subscript merepresentasikan expr[index]
    • Setiap pemanggilan virtual memakai makro SPECIALIZE_NEW_RMW
      • SetRMW adalah id += value
      • DotSetRMW adalah expr.id += value
      • SubscriptRMW adalah expr[index] += value
    • Spesialisasi operator pada perubahan #1 memakai dispatch lambda
    • Untuk RMW digunakan enum
      • Dipilih karena harus menangani ketiga jalur get, dot, dan subscript, serta meneruskan enum ke beberapa tempat
      • Pada akhirnya, fungsi template Value::callRMW<> yang melakukan dispatch pemanggilan operator RMW yang sebenarnya
    • Dampak performanya adalah peningkatan 3,7%
      • Pada titik ini, performanya 29 kali lebih lambat dari CPython 3.10
      • 65 kali lebih lambat dari Lua 5.4.7
      • 18,5 kali lebih lambat dari QuickJS-ng 0.14.0
      • 1,22 kali lebih cepat dibanding titik awal
  • Optimasi #3: menghindari pemeriksaan IntObject

    • Bottleneck-nya adalah jalur cepat Value memakai isInt(), dan di dalamnya isIntSlow() melakukan pemanggilan virtual Object::isInt()
    • Representasi nilai awalnya memiliki empat kasus
      • tagged int32
      • tagged double
      • IntObject untuk int64 yang tidak bisa direpresentasikan sebagai int32
      • semua objek lainnya
    • Bahkan untuk kasus IntObject, dispatch metode bilangan bulat tetap ditangani oleh Value
      • Tujuannya agar semua implementasi operasi aritmetika berada di satu tempat, yaitu Value
    • Setelah optimasi, jalur cepat Value hanya mempertimbangkan int32 dan double
      • Logika penanganan IntObject dipindahkan ke IntObject itu sendiri
      • Dengan begitu, pemanggilan isInt() yang sebelumnya terjadi pada setiap dispatch metode bisa dihindari
    • Dampak performanya adalah peningkatan 1%
      • Pada titik ini, performanya 29 kali lebih lambat dari CPython 3.10
      • 65 kali lebih lambat dari Lua 5.4.7
      • 18 kali lebih lambat dari QuickJS-ng 0.14.0
      • 1,23 kali lebih cepat dibanding titik awal
  • Optimasi #4: Symbol

    • Awalnya, interpreter memakai std::string hampir di semua tempat
    • Lokasi penggunaan string yang mahal adalah Context::get, Context::set, Context::callFunction, Value::callMethod, Value::dot, Value::setDot, Value::callOperator<>, dan kelompok Object::callMethod
    • Dengan struktur seperti ini, yang terjadi bukan sekadar lookup hashtable sederhana, melainkan lookup hashtable dengan kunci string, sehingga selama eksekusi terjadi hashing dan perbandingan string berulang
    • Optimasi ini mengganti lookup berbasis string dengan pointer objek Symbol hash-consed
    • Ditambahkan kelas Symbol baru
      • Diimplementasikan dalam symbol.h dan symbol.cpp
      • Symbol dan string bisa saling dikonversi
      • Saat mengubah string menjadi Symbol, dilakukan hash consing melalui hashtable global
      • Hasilnya, cukup dengan membandingkan identitas pointer Symbol* untuk menentukan apakah simbolnya sama
    • Alih-alih literal string, digunakan symbol yang sudah disiapkan sebelumnya
      • Misalnya, memakai Symbol::subscript alih-alih "subscript"
    • Banyak signature fungsi diubah agar memakai Symbol* alih-alih const std::string&
    • Dampak performanya adalah peningkatan 18%
      • Pada titik ini, performanya 24 kali lebih lambat dari CPython 3.10
      • 54 kali lebih lambat dari Lua 5.4.7
      • 15 kali lebih lambat dari QuickJS-ng 0.14.0
      • 1,46 kali lebih cepat dibanding titik awal
  • Optimasi #5: inlining Value

    • Inti perubahannya adalah memungkinkan inlining untuk fungsi-fungsi penting
    • Hampir semua perubahan berpusat pada penambahan header baru valueinlines.h
    • Alasan dipisah menjadi header terpisah dari value.h adalah karena ia memakai header-header yang perlu menyertakan value.h
    • Dampak performanya adalah peningkatan 2,8%
      • Pada titik ini, performanya 24 kali lebih lambat dari CPython 3.10
      • 53 kali lebih lambat dari Lua 5.4.7
      • 15 kali lebih lambat dari QuickJS-ng 0.14.0
      • 1,5 kali lebih cepat dibanding titik awal

Redesain model objek dan struktur cache

  • Optimasi #6: model objek, inline cache, Watchpoint

    • Merombak secara besar-besaran cara kerja Object, ClassObject, dan Context untuk menurunkan biaya alokasi objek dan menghindari pencarian hash table saat akses
    • Perubahan ini menggabungkan tiga fitur: model objek, inline cache, dan watchpoint
  • Model objek

    • Sebelumnya, sebuah objek Context dialokasikan untuk setiap lexical scope
      • Setiap Context memiliki hash table yang menyimpan variabel-variabel dalam scope tersebut
    • Objek memiliki struktur yang lebih kompleks
      • Setiap objek memiliki hash table yang memetakan kelas-kelas yang menjadi instansinya ke Context
    • Struktur ini diperlukan karena pewarisan dan nested scope
      • Saat Bar mewarisi Foo, Bar dan Foo menutup-over scope yang berbeda
      • Keduanya juga bisa memiliki field private berbeda dengan nama yang sama
    • Struktur baru memperkenalkan konsep Storage
      • Data disimpan berdasarkan Offsets
      • Offset ditentukan oleh suatu Context
    • Context tetap ada, tetapi tidak lagi dibuat saat objek atau scope dibuat, melainkan dibuat lebih dulu pada pass resolve di AST
    • Saat objek atau scope benar-benar dibuat, yang dialokasikan hanya Storage sesuai ukuran yang telah dihitung oleh Context tersebut
  • Inline cache

    • Teknik untuk mengingat tipe dinamis expr terakhir yang terlihat dan offset terakhir tempat name di-resolve pada lokasi kode seperti expr.name
    • Ini adalah teknik klasik yang umumnya dijelaskan dalam konteks JIT, tetapi di sini diterapkan pada interpreter
    • Informasi yang diingat diimplementasikan dengan melakukan placement construct node AST terspesialisasi di atas node AST biasa
  • Komponen inline cache

    • CacheRecipe
      • Melacak apa yang dilakukan oleh akses tertentu, dan apakah akses itu bisa di-cache
    • Pemanggilan CacheRecipe disisipkan di berbagai bagian Context, ClassObject, Package
      • Mengumpulkan informasi proses akses
    • Fungsi evaluasi AST seperti Dot::evaluate meneruskan CacheRecipe yang diperoleh dari operasi polimorfik yang dijalankannya bersama this ke constructCache<>
    • constructCache
      • Mengompilasi spesialisasi node AST baru berdasarkan CacheRecipe
      • Menghasilkan berbagai node AST terspesialisasi melalui mesin template
      • Jika yang diakses adalah variabel lokal, dilakukan load langsung terhadap storage yang diterima
      • Melakukan class check untuk memastikan kelasnya sama dengan yang terakhir terlihat
      • Setelah itu, melakukan pemanggilan fungsi langsung ke fungsi terakhir yang terlihat
      • Jika perlu, menggabungkan chain step dan watchpoint
    • Setiap node AST yang menjadi target cache memiliki cached variant masing-masing
      • Mula-mula mencoba pemanggilan cepat melalui objek cache
      • Tipe objek cache ditentukan oleh constructCache<>
  • watchpoint

    • Diberikan contoh lexical scope yang memiliki variabel x, kelas Foo di dalamnya, dan metode Foo yang mengakses x
    • Jika tidak ada fungsi atau variabel bernama x di dalam Foo, sekilas tampak bahwa ia bisa langsung membaca x dari luar
    • Namun, subclass bisa menambahkan getter x
    • Dalam kasus itu, hasil akses seharusnya bukan x luar, melainkan getter
    • Untuk menangani kemungkinan perubahan seperti ini, inline cache memasang Watchpoint saat runtime
    • Dalam contoh tersebut, digunakan watchpoint untuk memantau apakah nama ini telah dioverride
  • Alasan ketiganya diimplementasikan sekaligus

    • Dengan model objek baru saja, sulit mendapatkan peningkatan yang berarti jika inline cache tidak bekerja dengan baik
    • Inline cache juga kurang berguna tanpa watchpoint, karena banyak kondisi cache sulit ditangani dengan aman
    • Model objek baru dan watchpoint harus bekerja baik secara bersama-sama
  • Progres implementasi dan bagian yang sulit

    • Pengerjaan dimulai dari menulis versi sederhana CacheRecipe, serta merancang Storage dan Offsets yang sudah mendekati bentuk final
    • Salah satu pekerjaan tersulit adalah mengganti cara implementasi intrinsic class
    • Contoh array
      • Sebelumnya, ArrayObject::tryCallMethod mengimplementasikan semua metode dengan mencegat virtual call Object::tryCallMethod
      • Dalam model objek baru, Object tidak lagi memiliki vtable maupun metode virtual
      • Sebagai gantinya, Object::tryCallMethod mendelegasikan ke object->classObject()->tryCallMethod(object, ...)
      • Karena itu, untuk menyediakan metode Array, perlu dibuat kelas Array itu sendiri yang memiliki metode-metode tersebut
    • Akibatnya, banyak fungsi intrinsic berpindah dari struktur yang tersebar di seluruh implementasi menjadi terpusat pada makerootcontext.cpp
    • Ini dinilai sebagai hasil positif karena inline cache tetap berlaku apa adanya bahkan untuk fungsi native/intrinsic pada objek
    • Efek kinerjanya adalah peningkatan 4,55x
      • Pada titik ini, performanya 5,2x lebih lambat daripada CPython 3.10
      • 11,7x lebih lambat daripada Lua 5.4.7
      • 3,3x lebih lambat daripada QuickJS-ng 0.14.0
      • 6,8x lebih cepat dibanding titik awal
      • Selisih kerugian Fil-C++ dibanding interpreter lain dinilai telah menyempit hingga kurang lebih setara dengan tingkat biaya Fil-C

Optimasi jalur pemanggilan dan akses

  • Optimasi #7: Perbaikan struktur penerusan argumen

    • Sebelum perubahan, interpreter Zef meneruskan argumen fungsi sebagai const std::optional<std::vector<Value>>&
    • Alasan optional diperlukan adalah karena dalam beberapa kasus sudut perlu dibedakan dua hal berikut
      • o.getter
      • o.function()
    • Di Zef, pada umumnya keduanya sama-sama merupakan pemanggilan fungsi, tetapi ada pengecualian pada kode berikut
      • o.NestedClass
      • o.NestedClass()
    • Yang pertama mengembalikan objek NestedClass itu sendiri
    • Yang kedua membuat instance
    • Karena itu, perlu dibedakan antara pemanggilan fungsi tanpa argumen dan pemanggilan jenis getter dengan array argumen kosong
    • Namun, struktur lama tidak efisien
      • Pemanggil melakukan alokasi vector
      • Pihak yang dipanggil lalu mengalokasikan lagi arguments scope yang merupakan salinan dari vektor tersebut
    • Perubahan yang dilakukan adalah memperkenalkan tipe Arguments
      • Bentuknya persis sama dengan arguments scope yang sebelumnya dibuat oleh pihak yang dipanggil
      • Sekarang pemanggil langsung mengalokasikannya dalam bentuk itu
    • Bahkan di Yolo-C++, jumlah alokasi berkurang karena malloc untuk backing store vector dihapus
    • Di Fil-C++, std::optional itu sendiri dialokasikan di heap
      • Bahkan tanpa std::optional, penerusan const std::vector<>& juga tetap memicu alokasi
      • Hal yang seharusnya dialokasikan di stack secara eksplisit dinyatakan menjadi dialokasikan di heap
      • Disebutkan juga bahwa di sisi pemanggil, ukuran vektor tidak ditentukan lebih dulu sehingga terjadi realokasi berkali-kali
    • Sebagian besar perubahan ini adalah mengganti signature fungsi menjadi Arguments*
    • Dampak performanya adalah peningkatan 1,33x
      • Pada titik ini performanya 3,9x lebih lambat dari CPython 3.10
      • 8,8x lebih lambat dari Lua 5.4.7
      • 2,5x lebih lambat dari QuickJS-ng 0.14.0
      • 9,05x lebih cepat dibanding titik awal
  • Optimasi #8: Spesialisasi getter

    • Zef, mirip Ruby, memiliki field instance yang secara default bersifat private
    • Contoh class Foo { my f fn (inF) f = inF }
      • Menyimpan nilai yang diterima di konstruktor ke variabel lokal f yang hanya terlihat oleh instance
    • Bahkan antar instance dengan tipe yang sama, f milik objek lain tidak bisa diakses
      • Contoh fn nope(o) o.f
      • println(Foo(42).nope(Foo(666)))
      • o.f di dalam nope tidak bisa mengakses f milik o
    • Alasannya adalah karena field bekerja dengan cara muncul di rantai scope anggota kelas
      • o.f bukan pembacaan field, melainkan permintaan pemanggilan metode bernama f
    • Karena itu, pola berikut sering muncul
      • my f
      • fn f f
      • Artinya, metode bernama f yang mengembalikan variabel lokal f
    • Ada sintaks yang lebih singkat, yaitu readable f
      • Bentuk singkat dari my f dan fn f f
    • Banyak pemanggilan metode pada praktiknya sebenarnya adalah pemanggilan getter
    • Boros jika semua getter bekerja dengan mengevaluasi AST
    • Optimasi yang dilakukan adalah spesialisasi getter
      • Pusatnya adalah UserFunction
      • Dengan metode baru Node::inferGetter, diinfer apakah body fungsi adalah getter sederhana
    • Aturan inferensi
      • Block::inferGetter menginfer dirinya sebagai getter jika semua yang dikandungnya dapat diinfer sebagai getter
      • Get::inferGetter menginfer dirinya sebagai getter dan mengembalikan offset yang akan dimuat
      • Context::tryGetFieldOffsets hanya mengembalikan Offsets yang tidak kosong bila field tersebut pasti ada di scope leksikal tempat getter akan dijalankan
      • UserFunction akan di-resolve menjadi subclass Function khusus yang hanya membaca langsung dari offset yang sudah diketahui jika body fungsi bisa diinfer sebagai getter
    • Dampak performanya adalah peningkatan 5,6%
      • Pada titik ini performanya 3,7x lebih lambat dari CPython 3.10
      • 8,3x lebih lambat dari Lua 5.4.7
      • 2,4x lebih lambat dari QuickJS-ng 0.14.0
      • 9,55x lebih cepat dibanding titik awal
  • Optimasi #9: Spesialisasi setter

    • Dalam inferensi setter, perlu pattern matching untuk pola fn set_fieldName(newValue) fieldName = newValue
    • Pada tahap inferensi UserFunction, perlu meneruskan nama parameter setter
    • Pada tahap inferensi Set, perlu dipastikan bahwa ini bukan penulisan ke ClassObject, dan juga perlu dipastikan bahwa parameter setter digunakan sebagai sumber dari set tersebut
    • Dampak performanya adalah peningkatan 3,4%
      • Pada titik ini Zef 3,6x lebih lambat dari CPython 3.10
      • 8x lebih lambat dari Lua 5.4.7
      • 2,3x lebih lambat dari QuickJS-ng 0.14.0
      • 9,87x lebih cepat dibanding titik awal
  • Optimasi #10: Inline callMethod

    • Fungsi penting ini dijadikan inline hanya dengan perubahan satu baris
    • Dampak performanya adalah peningkatan 3,2%
      • Pada titik ini Zef 3,5x lebih lambat dari CPython 3.10
      • 7,8x lebih lambat dari Lua 5.4.7
      • 2,2x lebih lambat dari QuickJS-ng 0.14.0
      • 10,2x lebih cepat dibanding titik awal
  • Optimasi #11: Hash table

    • Ketika terjadi inline cache miss pada pemanggilan metode, eksekusi harus menelusuri ClassObject::tryCallMethod dan ClassObject::TryCallMethodDirect, dan kedua jalur ini sama-sama besar serta kompleks
    • Biaya pencarian sebelumnya adalah O(kedalaman hierarki), sebanding dengan kedalaman hierarki
      • Untuk setiap kelas dalam hierarki, dilakukan lookup hash table untuk memeriksa apakah pemanggilan itu di-resolve sebagai fungsi anggota
      • Untuk setiap kelas dalam hierarki, dilakukan juga lookup hash table untuk memeriksa apakah pemanggilan itu di-resolve sebagai kelas bersarang
    • Perubahan baru memperkenalkan hash table global yang menggunakan receiver class dan symbol sebagai kunci
      • Dengan satu kali lookup, callee dikembalikan secara langsung
      • Di classobject.h, tabel global ini dicek lebih dulu sebelum turun ke seluruh tryCallMethodSlow
      • Di classobject.cpp, hasil lookup yang berhasil dicatat ke tabel global
      • Implementasi hash table global itu sendiri relatif sederhana
    • Dampak performanya adalah peningkatan 15%
      • Pada titik ini Zef 3x lebih lambat dari CPython 3.10
      • 6,8x lebih lambat dari Lua 5.4.7
      • 1,9x lebih lambat dari QuickJS-ng 0.14.0
      • 11,8x lebih cepat dibanding titik awal
  • Optimasi #12: Menghindari std::optional

    • Di Fil-C++, patologi compiler terkait union membuat std::optional perlu dialokasikan di heap
    • Secara umum LLVM menangani tipe akses memori union dengan longgar, tetapi ini berbenturan dengan invisicaps
      • Ada kasus ketika pointer di dalam union kehilangan capability dengan cara yang sulit diprediksi dari sudut pandang programmer
      • Akibatnya, di Fil-C dapat terjadi panic karena dereference objek dengan null capability meski tanpa kesalahan programmer
    • Untuk meredam hal ini, compiler Fil-C++ menyisipkan intrinsics agar LLVM bertindak konservatif saat menangani variabel lokal bertipe union
    • Setelah itu, pass FilPizlonator melakukan escape analysis sendiri untuk mencoba membuat variabel lokal bertipe union bisa dialokasikan ke register
      • Namun analisis ini tidak selengkap analisis SROA milik LLVM biasa
    • Akibatnya, di Fil-C++ penerusan kelas yang mengandung union seperti std::optional sering berujung pada alokasi memori
    • Perubahan kali ini menghindari jalur kode di hot path yang berujung pada std::optional
    • Dampak performanya adalah peningkatan 1,7%
      • Pada titik ini Zef 3x lebih lambat dari CPython 3.10
      • 6,65x lebih lambat dari Lua 5.4.7
      • 1,9x lebih lambat dari QuickJS-ng 0.14.0
  • 12 kali lebih cepat dibanding titik awal

  • Optimisasi #13: argumen terspesialisasi

    • Semua fungsi built-in di Zef menerima 1 atau 2 argumen, dan pada implementasi native tidak perlu mengalokasikan objek Arguments untuk menampungnya
    • Setter juga selalu menerima satu argumen, dan jika inferensi setter telah dilakukan, implementasi setter terspesialisasi juga cukup menerima argumen nilai secara langsung tanpa objek Arguments
    • Perubahan kali ini memperkenalkan tipe argumen terspesialisasi ZeroArguments, OneArgument, TwoArguments
      • Jika callee tidak membutuhkannya, caller bisa menghindari alokasi objek Arguments
    • ZeroArguments diperlukan untuk membedakannya dari (Arguments*)nullptr
      • Sebelumnya, (Arguments*)nullptr digunakan dengan makna pemanggilan getter, dan logika itu tetap dipertahankan
      • Kini ZeroArguments berarti pemanggilan fungsi tanpa argumen
    • Banyak perubahan terdiri dari pekerjaan menjadikan fungsi yang menerima argumen sebagai template
      • Untuk masing-masing ZeroArguments, OneArgument, TwoArguments, Arguments*, dilakukan instansiasi eksplisit
      • Sebagian besar kode lama menggunakan Value::getArg sebagai helper ekstraksi argumen, dan di sini ditambahkan overload argumen terspesialisasi
      • Perubahan pada kode native yang menggunakan argumen relatif merupakan modifikasi yang lugas
    • Dampak pada performa adalah peningkatan 3,8%
      • Pada titik ini, Zef 2,9 kali lebih lambat daripada CPython 3.10
      • 6,4 kali lebih lambat daripada Lua 5.4.7
      • 1,8 kali lebih lambat daripada QuickJS-ng 0.14.0
      • 12,4 kali lebih cepat dibanding titik awal

Mengakali patologi Fil-C dan spesialisasi yang lebih rinci

  • Optimisasi #14: slow path Value yang ditingkatkan

    • Mendapatkan peningkatan kecepatan besar lewat pengakalan patologi Fil-C lainnya
    • Sebelum perubahan, slow path out-of-line milik Value adalah fungsi anggota Value, dan memerlukan argumen implisit const Value*
    • Dalam struktur ini, caller harus mengalokasikan Value di stack
    • Di Fil-C++, semua alokasi stack adalah alokasi heap
      • Karena itu, kode yang memanggil slow path akan mengalokasikan Value di heap
    • Setelah perubahan, metode-metode ini diubah menjadi static, dan Value dikirim berdasarkan nilai
      • Hasilnya, tidak perlu alokasi terpisah
    • Dampak performanya adalah peningkatan 10%
      • Pada titik ini, Zef 2,6 kali lebih lambat dari CPython 3.10
      • 5,8 kali lebih lambat dari Lua 5.4.7
      • 1,65 kali lebih lambat dari QuickJS-ng 0.14.0
      • 13,6 kali lebih cepat dibanding titik awal
  • Optimisasi #15: deduplikasi DotSetRMW

    • Melakukan penghapusan sebagian kode duplikat
    • Diperkirakan pengurangan kode mesin dapat menguntungkan pada fungsi template yang dispesialisasi oleh constructCache<>
    • Hasil nyatanya tidak ada dampak pada performa
  • Optimisasi #16: spesialisasi sqrt

    • Inline cache dapat merutekan pemanggilan ke fungsi yang diinginkan dengan baik, tetapi hanya bekerja untuk objek
    • Pada non-objek, fast path Binary<>, Unary<>, dan Value::callRMW<> bergantung pada cara memeriksa apakah receiver adalah int atau double
    • Cara ini hanya berlaku untuk operator yang dikenali parser
      • Tidak berlaku untuk bentuk seperti value.sqrt
    • Dengan perubahan ini, Dot bisa dispesialisasi untuk value.sqrt
    • Dampak performanya adalah peningkatan 1,6%
      • Pada titik ini, Zef 2,6 kali lebih lambat dari CPython 3.10
      • 5,75 kali lebih lambat dari Lua 5.4.7
      • 1,6 kali lebih lambat dari QuickJS-ng 0.14.0
      • 13,8 kali lebih cepat dibanding titik awal
  • Optimisasi #17: spesialisasi toString

    • Menerapkan spesialisasi toString dengan cara yang hampir sama seperti optimisasi sebelumnya
    • Perubahan ini juga mencakup logika pengurangan jumlah alokasi saat mengubah int menjadi string
    • Dampak performanya adalah peningkatan 2,7%
      • Pada titik ini, Zef 2,5 kali lebih lambat dari CPython 3.10
      • 5,6 kali lebih lambat dari Lua 5.4.7
      • 1,6 kali lebih lambat dari QuickJS-ng 0.14.0
      • 14,2 kali lebih cepat dibanding titik awal
  • Optimisasi #18: spesialisasi literal array

    • Kode seperti my whatever = [1, 2, 3] memerlukan alokasi array baru di Zef karena array bersifat dapat di-alias dan mutable
    • Sebelum perubahan, setiap kali dijalankan ia menelusuri AST dan mengevaluasi 1, 2, 3 secara rekursif setiap saat
    • Perubahan kali ini membuat node ArrayLiteral dapat dispesialisasi untuk kasus alokasi array konstan
    • Dampak performanya adalah peningkatan 8,1%
      • Pada titik ini, Zef 2,3 kali lebih lambat dari CPython 3.10
      • 5,2 kali lebih lambat dari Lua 5.4.7
      • 1,5 kali lebih lambat dari QuickJS-ng 0.14.0
      • 15,35 kali lebih cepat dibanding titik awal
  • Optimisasi #19: peningkatan Value::callOperator

    • Menerapkan optimisasi yang sama seperti sebelumnya—yang memberi peningkatan kecepatan dengan tidak mengirim Value sebagai referensi—ke slow path callOperator juga
    • Dampak performanya adalah peningkatan 6,5%
      • Pada titik ini, Zef 2,2 kali lebih lambat dari CPython 3.10
      • 4,9 kali lebih lambat dari Lua 5.4.7
      • 1,4 kali lebih lambat dari QuickJS-ng 0.14.0
      • 16,3 kali lebih cepat dibanding titik awal
  • Optimisasi #20: opsi C++ yang lebih baik

    • Di Fil-C++, RTTI yang tidak perlu dan libc++ hardening dinonaktifkan
    • Tidak ada perubahan pada kode C++ itu sendiri, hanya perubahan konfigurasi sistem build
    • Dampak performanya adalah peningkatan 1,8%
      • Pada titik ini, Zef 2,1 kali lebih lambat dari CPython 3.10
      • 4,8 kali lebih lambat dari Lua 5.4.7
      • 1,35 kali lebih lambat dari QuickJS-ng 0.14.0
      • 16,6 kali lebih cepat dibanding titik awal
  • Optimisasi #21: menonaktifkan assert

    • Sebagai optimisasi terakhir, diterapkan penonaktifan assertion secara default
    • Kode sebelumnya menggunakan makro ZASSERT khusus Fil-C
      • Strukturnya selalu menjalankan assert
    • Setelah perubahan, digunakan makro internal ASSERT
      • Assert hanya dijalankan jika ASSERTS_ENABLED disetel
    • Perubahan ini juga mencakup modifikasi lain agar kode bisa dibangun di Yolo-C++
    • Berlawanan dengan harapan, tidak ada peningkatan kecepatan

Hasil dan keterbatasan Yolo-C++

  • Hasil kompilasi kode dengan Yolo-C++ memberikan peningkatan kecepatan 4 kali lipat
  • Namun, pendekatan ini tidak sound dan suboptimal
    • Tidak sound karena pemanggilan GC Fil-C++ yang ada berubah menjadi pemanggilan calloc
    • Akibatnya memori tidak dibebaskan, dan pada workload yang berjalan cukup lama interpreter akan kehabisan memori
    • Pada ScriptBench1, waktu pengujian pendek sehingga tidak terjadi kehabisan memori
  • Disebut suboptimal karena allocator GC yang sebenarnya lebih cepat daripada calloc milik glibc 2.35
  • Karena itu, disebutkan bahwa jika GC sungguhan ditambahkan ke port Yolo-C++, peningkatan kecepatan yang didapat bisa lebih besar dari 4 kali
  • Eksperimen ini menggunakan GCC 11.4.0
  • Pada titik ini, Zef adalah
    • 1,9 kali lebih cepat dari CPython 3.10

    • 1,2 kali lebih lambat dari Lua 5.4.7

    • 3 kali lebih cepat dari QuickJS-ng 0.14.0

      • 67 kali lebih cepat dibanding titik awal

Data benchmark mentah

  • Satuan waktu eksekusi benchmark adalah detik
  • Tabel mencakup nbody, splay, richards, deltablue, geomean untuk tiap interpreter
  • 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 Baseline

    • nbody 2.9573
    • splay 13.0286
    • richards 1.9251
    • deltablue 5.9997
    • geomean 4.5927
  • Zef Perubahan #1: Operator Langsung

    • nbody 2.1891
    • splay 12.0233
    • richards 1.6935
    • deltablue 5.2331
    • geomean 3.9076
  • Zef Perubahan #2: RMW Langsung

    • nbody 2.0130
    • splay 11.9987
    • richards 1.6367
    • deltablue 5.0994
    • geomean 3.7677
  • Zef Perubahan #3: Hindari IntObject

    • nbody 1.9922
    • splay 11.8824
    • richards 1.6220
    • deltablue 5.0646
    • geomean 3.7339
  • Zef Perubahan #4: Symbol

    • nbody 1.5782
    • splay 9.9577
    • richards 1.4116
    • deltablue 4.4593
    • geomean 3.1533
  • Zef Perubahan #5: Value Inline

    • nbody 1.4982
    • splay 9.7723
    • richards 1.3890
    • deltablue 4.3536
    • geomean 3.0671
  • Zef Perubahan #6: Model Objek dan Inline Cache

    • nbody 0.3884
    • splay 3.3609
    • richards 0.2321
    • deltablue 0.6805
    • geomean 0.6736
  • Zef Perubahan #7: Argumen

    • nbody 0.3160
    • splay 2.6890
    • richards 0.1653
    • deltablue 0.4738
    • geomean 0.5077
  • Zef Perubahan #8: Getter

    • nbody 0.2988
    • splay 2.6919
    • richards 0.1564
    • deltablue 0.4260
    • geomean 0.4809
  • Zef Perubahan #9: Setter

    • nbody 0.2850
    • splay 2.6690
    • richards 0.1514
    • deltablue 0.4072
    • geomean 0.4651
  • Zef Perubahan #10: callMethod inline

    • nbody 0.2533
    • splay 2.6711
    • richards 0.1513
    • deltablue 0.4032
    • geomean 0.4506
  • Zef Perubahan #11: Hashtable

    • nbody 0.1796
    • splay 2.6528
    • richards 0.1379
    • deltablue 0.3551
    • geomean 0.3906
  • Zef Perubahan #12: Hindari std::optional

    • nbody 0.1689
    • splay 2.6563
    • richards 0.1379
    • deltablue 0.3518
    • geomean 0.3839
  • Zef Perubahan #13: Argumen Terspesialisasi

    • nbody 0.1610
    • splay 2.5823
    • richards 0.1350
    • deltablue 0.3372
    • geomean 0.3707
  • Zef Perubahan #14: Slow Path Value yang Ditingkatkan

    • nbody 0.1348
    • splay 2.5062
    • richards 0.1241
    • deltablue 0.3076
    • geomean 0.3367
  • Zef Perubahan #15: DotSetRMW::evaluate yang Dideduplikasi

    • nbody 0.1342
    • splay 2.5047
    • richards 0.1256
    • deltablue 0.3079
    • geomean 0.3375
  • Zef Perubahan #16: sqrt cepat

    • nbody 0.1274
    • splay 2.5045
    • richards 0.1251
    • deltablue 0.3060
    • geomean 0.3322
  • Zef Perubahan #17: toString cepat

    • nbody 0.1282
    • splay 2.2664
    • richards 0.1275
    • deltablue 0.2964
    • geomean 0.3235
  • Zef Perubahan #18: Spesialisasi Literal Array

    • nbody 0.1295
    • splay 1.6661
    • richards 0.1250
    • deltablue 0.2979
    • geomean 0.2992
  • Zef Perubahan #19: Optimisasi callOperator pada Value

    • nbody 0.1208
    • splay 1.6698
    • richards 0.1143
    • deltablue 0.2713
    • geomean 0.2810
  • Zef Perubahan #20: Konfigurasi C++ yang Lebih Baik

    • nbody 0.1186
    • splay 1.6521
    • richards 0.1127
    • deltablue 0.2635
    • geomean 0.2760
  • Zef Perubahan #21: Tanpa Assert

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

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

1 komentar

 
GN⁺ 2 jam lalu
Opini Hacker News
  • Dalam konteks yang mirip, halaman ini tentang performa interpreter Wren cukup menarik
    Jika tulisan Zef berfokus pada teknik implementasi, sisi Wren menurut saya juga menunjukkan bagaimana desain bahasa itu sendiri berkontribusi pada performa
    Terutama, saya suka bahwa Wren mengorbankan dynamic object shapes sehingga copy-down inheritance menjadi mungkin dan pencarian metode jauh lebih sederhana
    Secara pribadi saya melihat ini sebagai trade-off yang cukup bagus. Seberapa sering sih kita benar-benar perlu menambahkan metode ke sebuah kelas setelah kelas itu dibuat?

    • Menurut saya, kecepatan interpreter atau JIT sangat besar ditentukan oleh desain bahasa
      Ada banyak VM yang sangat dioptimalkan untuk bahasa dinamis, tetapi saya merasa LuaJIT kuat karena Lua sendiri memang sangat kecil dan sangat cocok untuk dioptimalkan
      Memang ada beberapa fitur yang sulit dioptimalkan, tetapi jumlahnya sedikit sehingga layak diberi perhatian khusus
      Sebaliknya, Python terasa sangat berbeda. Sedikit melebih-lebihkan, rasanya Python dirancang untuk meminimalkan kemungkinan adanya JIT cepat, dan lapisan-lapisan dinamismenya membuat optimisasi benar-benar sulit
      Fakta bahwa bahkan setelah dikerjakan selama itu, JIT CPython 3.15 di x86_64 hanya sekitar 5% lebih cepat daripada interpreter dasarnya menurut saya menunjukkan hal itu dengan baik
    • Pendekatan seperti ini menurut saya mirip dengan hal yang selalu dilakukan di bahasa-bahasa yang secara idiomatik menerima monkey patching, terutama Ruby
      Tentu saja, sekaligus mengingatkan bahwa Ruby bukan bahasa yang dikenal sangat berorientasi pada kecepatan
      Sebaliknya, gagasan bahwa sebuah tipe memiliki himpunan fungsi yang berlaku padanya dalam bentuk tertutup juga terasa agak meragukan
      Ada cukup banyak bahasa di dunia yang memungkinkan fungsi arbitrer didefinisikan lalu dipakai seperti metode dengan notasi titik pada variabel yang tipe argumen pertamanya cocok
      Misalnya makro di Nim, implicit classes dan type classes di Scala, extension functions di Kotlin, serta traits di Rust
    • Dalam pengalaman saya, jika sebuah ekspresi bisa diberi tipe statis, maka biasanya ia bisa dikompilasi dengan cukup efisien
      Bahasa dinamis yang kompleks cenderung aktif merusak kemungkinan itu dengan berbagai cara, sehingga optimisasinya jadi sulit
      Kalau dipikir-pikir lagi, itu terdengar cukup jelas
  • Saat berpindah dari perubahan #5 ke #6, fakta bahwa inline caches dan model objek hidden-class menghasilkan sebagian besar peningkatan performa terasa sangat mirip secara historis dengan cara V8 atau JSC menjadi cepat
    Titik kematian bagi interpreter naif pada akhirnya adalah dynamic dispatch untuk akses properti, dan sisanya memberi kesan relatif mendekati rounding error
    Saya juga suka karena ditata sehingga kita bisa melihat seberapa besar kontribusi tiap tahap secara terpisah. Biasanya tulisan performa hanya melempar angka akhir lalu selesai

    • Detail implementasi yang sangat menarik di #6 adalah bagaimana melakukan inline caching pada interpreter yang langsung menelusuri AST
      Dalam interpreter bytecode, cukup dengan menambal offset stabil di stream bytecode, jadi lokasi penulisan ulang IC terasa alami
      Tetapi di sini posisi cache-nya adalah node AST, jadi saya terkesan bahwa @pizlonator memakai constructCache<> untuk membangun node AST terspesialisasi secara in-place di atas node generik
      Pada akhirnya ini terlihat seperti self-modifying code di level AST
      Sebagai gantinya, cara ini membutuhkan mutable AST nodes, sehingga bertabrakan dengan asumsi AST immutable yang diharapkan banyak compiler untuk hal-hal seperti berbagi subtree atau kompilasi paralel
      Untuk interpreter single-threaded ini rapi, tetapi jika interpreter mengubah node sementara AST yang sama sedang dikompilasi JIT di thread latar belakang, rasanya itu bisa jadi masalah
    • Saya setuju dengan arah umumnya, tetapi menurut saya ada catatan kecil bahwa ini bagaimanapun juga hanya hasil untuk satu benchmark tertentu
      Menurut saya itu mungkin tidak mewakili sebagian besar kode kerja nyata dengan baik
      Saya merasa begitu karena ada bagian yang mengatakan optimisasi sqrt memberi peningkatan 1.6%
      Agar peningkatan sebesar itu muncul, berarti sejak awal benchmark memang menghabiskan setidaknya 1.6% waktunya di sana, dan itu cukup mengejutkan
      Setelah melihat repo git, tampaknya memang itu yang terjadi di simulasi nbody
  • Saya juga baru-baru ini merilis versi pertama AST-walking interpreter saya, jadi saya membacanya dengan lebih tertarik
    Tujuan saya adalah memahami di level dasar apa saja yang dibutuhkan untuk membuat bahasa interpreter
    Saya tidak ingin memasukkan kompleksitas optimisasi, hanya fokus membuat kode Rust saya bisa saya pahami sendiri
    Tetapi saya terkejut karena hanya dengan memakai Rust, yang merupakan bahasa favorit saya, performanya sudah cukup bagus
    Ditambah lagi, karena Rust mengurus ownership dan lifetime, rasanya seperti bonus bahwa saya tidak butuh garbage collector terpisah
    Tentu saja sekarang saya masih cukup konservatif bergantung pada clone untuk menghindari neraka lifetime di bagian seperti closure, tetapi tetap saja profil kecepatan dan memorinya terasa cukup baik
    Jika Anda tertarik pada tree-walking interpreter berbasis Rust yang sederhana dan mudah dipahami, lihat interpreter saya gluonscript

  • Tulisannya sangat bagus
    Terutama arc Arguments, yaitu alur dari #7 ke #13, terasa sangat dekat dengan pengalaman saya
    Dulu saat membuat async step evaluator di Rust, saya sangat percaya borrow biasanya akan memberi keuntungan sehingga sempat mendalami Cow<'_, Input>
    Di microbenchmark itu tampak bagus, tetapi di workload nyata kompleksitas discriminant dan lifetime pada Cow menyebar ke semua combinator setelah await pertama, inlining pun runtuh besar-besaran, dan alasan memakai Cow sendiri jadi hilang
    Akhirnya saya beralih di batas evaluator ke NoInput / OneInput / MultiInput(Vec), dan meski tampilannya lebih kasar, hasilnya pada akhirnya hampir sama dengan pemisahan ZeroArguments / OneArgument / TwoArguments di sini
    Satu hal yang terus saya penasaran adalah apakah di jalur native pernah dicoba menumpuk type specialization di atas arity specialization
    Misalnya, jika memakai gaya biner, mungkin pemeriksaan isInt itu sendiri bisa dihilangkan
    Dugaan saya, entah perhitungan ukuran kode tidak cocok, atau di sisi objek IC sudah cukup memakan jalur panas sehingga fast path native tidak terlalu berpengaruh
    Saya penasaran yang mana

  • Ini benar-benar pekerjaan yang menarik dan dilakukan dengan baik
    Saya juga pernah melakukan hal serupa, tetapi di sisi Scheme yang lebih dekat ke bahasa fungsional
    Di sini optimisasi objek memberi keuntungan terbesar, tetapi dalam kasus saya optimisasi closures adalah medan pertempuran utamanya
    Menariknya, cara optimisasinya sendiri cukup mirip
    Menurut saya, hampir semua jawaban untuk membuat Scheme cukup cepat ada di Three implementation models for scheme
    Hanya saja, sisi ini sampai tingkat tertentu melewati tahap kompilasi, jadi berbeda dari model yang benar-benar menginterpretasikan AST asli apa adanya

  • Menarik, dan terima kasih sudah membagikannya
    Ini membuat saya berpikir bahwa suatu hari saya juga ingin menggali topik ini secara mendalam
    Dan menurut Github, fakta bahwa repo ini 99.7% HTML dan 0.3% C++ juga cukup lucu dan berkesan
    Rasanya itu bukti bahwa ukuran interpreternya memang sangat kecil

    • Itu terlihat begitu karena situs yang digenerasikan secara statis ikut dikomit
      Karena cara menghasilkan kode untuk browser, sisi situsnya jadi membesar tanpa perlu
      Tapi interpreter-nya sendiri memang sangat kecil
  • Saya penasaran apakah selama mengerjakan ini ada hal yang dipelajari yang bisa membuat fil c sendiri jadi lebih baik

    • Saya jelas merasa bahwa cara menangani unions membutuhkan solusi yang lebih baik
      Dan saya juga belajar bahwa biaya menangani metode value object sebagai outline call cukup besar
  • Saya lihat Lua disertakan, tetapi saya merasa akan bagus jika LuaJIT juga ada

    • Dugaan saya, LuaJIT akan benar-benar mengalahkan Zef
      Bahkan, mengingat tingkat rekayasa yang masuk ke sana, saya malah berharap memang begitu
      Ada banyak runtime yang sebenarnya bisa dimasukkan, tetapi tidak semuanya disertakan
      Dan fakta bahwa PUC Lua jauh lebih cepat daripada QuickJS atau Python juga cukup mengesankan
  • Saya penasaran bagaimana pengalaman benar-benar memakai Fil-C, dan apakah ada kegunaan praktis di dunia nyata

    • Saya perlu bilang dulu bahwa saya adalah Fil sendiri, jadi ada bias
      Meski begitu, di proyek ini itu cukup membantu secara praktis
      Ia menangkap beberapa masalah keamanan memori secara deterministik, sehingga desain model objek menjadi jauh lebih mudah daripada jika itu tidak ada
      Selain itu, C++ dengan GC yang presisi terasa seperti model pemrograman yang sangat bagus
      Produktivitas saya terasa naik sekitar 1.5x dibanding C++ biasa, dan bahkan dibanding bahasa GC lain rasanya pengembangan tetap sekitar 1.2x lebih cepat
      Menurut saya alasannya karena ekosistem API C++ sangat kaya dan lambdas, templates, serta class system-nya sangat matang
      Tentu saja saya akui ada bias dalam banyak hal
      Saya sendiri yang membuat Fil-C++, dan saya juga sudah memakai C++ sekitar 35 tahun
  • Saya penasaran apa yang dimaksud dengan compiler YOLO-C/C++ yang disebut di tulisan
    Bahkan setelah mencari, hampir tidak ada hasil dan chatgpt juga tampaknya tidak tahu

    • Penulis Fil-C dan juga bahasa ini memakai istilah Yolo-C/C++ untuk merujuk pada C/C++ biasa tanpa Fil-C