Cara Membuat Interpreter Bahasa Dinamis yang Cepat
(zef-lang.dev)- 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·toStringyang 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*
- Nilai yang dapat ditampung adalah double, integer 32-bit, dan
- 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
- Bergantung pada asumsi bahwa nilai pointer tidak lebih kecil dari
- 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
- Menggunakan tagged value 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
- Disebutkan bahwa sebagian atau seluruh implementasi masih mungkin memakai Rust jika menerima konfigurasi multi-bahasa atau mengizinkan banyak kode
-
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::evaluateyang dioverride di banyak tempat
- Menggunakan struktur metode virtual
- Penyalahgunaan string
- Node AST
Getmenyimpanstd::stringyang menjelaskan nama variabel - String itu digunakan setiap kali variabel diakses
- Node AST
- Penyalahgunaan hash table
- Saat
Getdijalankan, dilakukan lookupstd::unordered_mapdengan key string
- Saat
- 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
- Menggunakan Fil-C++
-
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
DotCalldengan nama operator, melainkan membuat node AST terpisah untuk setiap operator - Di Zef,
a + bdana.add(b)itu sama- Awalnya,
a + bdi-parse sebagaiDotCall(a, "add")dengan argumenb - Pada setiap operasi aritmetika, terjadi pencarian string nama metode operator
DotCallmeneruskan string keValue::callMethodValue::callMethodmelakukan beberapa perbandingan string
- Awalnya,
- Setelah diubah, parser membuat node
Binary<>danUnary<>- Dengan memanfaatkan template dan lambda, disediakan override
Node::evaluateyang berbeda untuk tiap operator - Setiap node langsung memanggil jalur cepat
Valueuntuk operator tersebut - Contohnya,
a + bmemanggilBinary<lambda for add>::evaluatelaluValue::add
- Dengan memanfaatkan template dan lambda, disediakan override
- 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
- Parser tidak lagi membuat operator sebagai node
-
Optimasi #2: pemanggilan operator RMW langsung
- Operator biasa sudah lebih cepat, tetapi bentuk RMW seperti
a += bmasih 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]
- Get merepresentasikan pembacaan variabel
- Setiap pemanggilan virtual memakai makro
SPECIALIZE_NEW_RMW- SetRMW adalah
id += value - DotSetRMW adalah
expr.id += value - SubscriptRMW adalah
expr[index] += value
- SetRMW adalah
- Spesialisasi operator pada perubahan #1 memakai dispatch lambda
- Untuk RMW digunakan enum
- Dipilih karena harus menangani ketiga jalur
get,dot, dansubscript, serta meneruskan enum ke beberapa tempat - Pada akhirnya, fungsi template
Value::callRMW<>yang melakukan dispatch pemanggilan operator RMW yang sebenarnya
- Dipilih karena harus menangani ketiga jalur
- 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
- Operator biasa sudah lebih cepat, tetapi bentuk RMW seperti
-
Optimasi #3: menghindari pemeriksaan IntObject
- Bottleneck-nya adalah jalur cepat
ValuememakaiisInt(), dan di dalamnyaisIntSlow()melakukan pemanggilan virtualObject::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
- Tujuannya agar semua implementasi operasi aritmetika berada di satu tempat, yaitu
- Setelah optimasi, jalur cepat
Valuehanya mempertimbangkan int32 dan double- Logika penanganan IntObject dipindahkan ke
IntObjectitu sendiri - Dengan begitu, pemanggilan
isInt()yang sebelumnya terjadi pada setiap dispatch metode bisa dihindari
- Logika penanganan IntObject dipindahkan ke
- 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
- Bottleneck-nya adalah jalur cepat
-
Optimasi #4: Symbol
- Awalnya, interpreter memakai
std::stringhampir di semua tempat - Lokasi penggunaan string yang mahal adalah
Context::get,Context::set,Context::callFunction,Value::callMethod,Value::dot,Value::setDot,Value::callOperator<>, dan kelompokObject::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
Symbolhash-consed - Ditambahkan kelas
Symbolbaru- Diimplementasikan dalam
symbol.hdansymbol.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
- Diimplementasikan dalam
- Alih-alih literal string, digunakan symbol yang sudah disiapkan sebelumnya
- Misalnya, memakai
Symbol::subscriptalih-alih"subscript"
- Misalnya, memakai
- Banyak signature fungsi diubah agar memakai
Symbol*alih-alihconst 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
- Awalnya, interpreter memakai
-
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.hadalah karena ia memakai header-header yang perlu menyertakanvalue.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, danContextuntuk menurunkan biaya alokasi objek dan menghindari pencarian hash table saat akses - Perubahan ini menggabungkan tiga fitur: model objek, inline cache, dan watchpoint
- Merombak secara besar-besaran cara kerja
-
Model objek
- Sebelumnya, sebuah objek
Contextdialokasikan untuk setiap lexical scope- Setiap
Contextmemiliki hash table yang menyimpan variabel-variabel dalam scope tersebut
- Setiap
- Objek memiliki struktur yang lebih kompleks
- Setiap objek memiliki hash table yang memetakan kelas-kelas yang menjadi instansinya ke
Context
- Setiap objek memiliki hash table yang memetakan kelas-kelas yang menjadi instansinya ke
- Struktur ini diperlukan karena pewarisan dan nested scope
- Saat
BarmewarisiFoo,BardanFoomenutup-over scope yang berbeda - Keduanya juga bisa memiliki field private berbeda dengan nama yang sama
- Saat
- Struktur baru memperkenalkan konsep
Storage- Data disimpan berdasarkan Offsets
- Offset ditentukan oleh suatu
Context
Contexttetap ada, tetapi tidak lagi dibuat saat objek atau scope dibuat, melainkan dibuat lebih dulu pada passresolvedi AST- Saat objek atau scope benar-benar dibuat, yang dialokasikan hanya Storage sesuai ukuran yang telah dihitung oleh
Contexttersebut
- Sebelumnya, sebuah objek
-
Inline cache
- Teknik untuk mengingat tipe dinamis
exprterakhir yang terlihat dan offset terakhir tempatnamedi-resolve pada lokasi kode sepertiexpr.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
- Teknik untuk mengingat tipe dinamis
-
Komponen inline cache
CacheRecipe- Melacak apa yang dilakukan oleh akses tertentu, dan apakah akses itu bisa di-cache
- Pemanggilan
CacheRecipedisisipkan di berbagai bagianContext,ClassObject,Package- Mengumpulkan informasi proses akses
- Fungsi evaluasi AST seperti
Dot::evaluatemeneruskanCacheRecipeyang diperoleh dari operasi polimorfik yang dijalankannya bersamathiskeconstructCache<> 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
storageyang 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
- Mengompilasi spesialisasi node AST baru berdasarkan
- Setiap node AST yang menjadi target cache memiliki cached variant masing-masing
- Mula-mula mencoba pemanggilan cepat melalui objek
cache - Tipe objek
cacheditentukan olehconstructCache<>
- Mula-mula mencoba pemanggilan cepat melalui objek
-
watchpoint
- Diberikan contoh lexical scope yang memiliki variabel
x, kelasFoodi dalamnya, dan metodeFooyang mengaksesx - Jika tidak ada fungsi atau variabel bernama
xdi dalamFoo, sekilas tampak bahwa ia bisa langsung membacaxdari luar - Namun, subclass bisa menambahkan getter
x - Dalam kasus itu, hasil akses seharusnya bukan
xluar, melainkan getter - Untuk menangani kemungkinan perubahan seperti ini, inline cache memasang
Watchpointsaat runtime - Dalam contoh tersebut, digunakan watchpoint untuk memantau apakah nama ini telah dioverride
- Diberikan contoh lexical scope yang memiliki variabel
-
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::tryCallMethodmengimplementasikan semua metode dengan mencegat virtual callObject::tryCallMethod - Dalam model objek baru,
Objecttidak lagi memiliki vtable maupun metode virtual - Sebagai gantinya,
Object::tryCallMethodmendelegasikan keobject->classObject()->tryCallMethod(object, ...) - Karena itu, untuk menyediakan metode
Array, perlu dibuat kelas Array itu sendiri yang memiliki metode-metode tersebut
- Sebelumnya,
- 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
- Pengerjaan dimulai dari menulis versi sederhana
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
optionaldiperlukan adalah karena dalam beberapa kasus sudut perlu dibedakan dua hal berikuto.gettero.function()
- Di Zef, pada umumnya keduanya sama-sama merupakan pemanggilan fungsi, tetapi ada pengecualian pada kode berikut
o.NestedClasso.NestedClass()
- Yang pertama mengembalikan objek
NestedClassitu 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
- Pemanggil melakukan alokasi
- 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
mallocuntuk backing store vector dihapus - Di Fil-C++,
std::optionalitu sendiri dialokasikan di heap- Bahkan tanpa
std::optional, penerusanconst 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
- Bahkan tanpa
- 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
- Sebelum perubahan, interpreter Zef meneruskan argumen fungsi sebagai
-
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
fyang hanya terlihat oleh instance
- Menyimpan nilai yang diterima di konstruktor ke variabel lokal
- Bahkan antar instance dengan tipe yang sama,
fmilik objek lain tidak bisa diakses- Contoh
fn nope(o) o.f println(Foo(42).nope(Foo(666)))o.fdi dalamnopetidak bisa mengaksesfmiliko
- Contoh
- Alasannya adalah karena field bekerja dengan cara muncul di rantai scope anggota kelas
o.fbukan pembacaan field, melainkan permintaan pemanggilan metode bernamaf
- Karena itu, pola berikut sering muncul
my ffn f f- Artinya, metode bernama
fyang mengembalikan variabel lokalf
- Ada sintaks yang lebih singkat, yaitu
readable f- Bentuk singkat dari
my fdanfn f f
- Bentuk singkat dari
- 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
- Pusatnya adalah
- Aturan inferensi
Block::inferGettermenginfer dirinya sebagai getter jika semua yang dikandungnya dapat diinfer sebagai getterGet::inferGettermenginfer dirinya sebagai getter dan mengembalikan offset yang akan dimuatContext::tryGetFieldOffsetshanya mengembalikanOffsetsyang tidak kosong bila field tersebut pasti ada di scope leksikal tempat getter akan dijalankanUserFunctionakan di-resolve menjadi subclassFunctionkhusus 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
- Dalam inferensi setter, perlu pattern matching untuk pola
-
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::tryCallMethoddanClassObject::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 seluruhtryCallMethodSlow - 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
- Ketika terjadi inline cache miss pada pemanggilan metode, eksekusi harus menelusuri
-
Optimasi #12: Menghindari
std::optional- Di Fil-C++, patologi compiler terkait union membuat
std::optionalperlu 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
FilPizlonatormelakukan 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::optionalsering 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
- Di Fil-C++, patologi compiler terkait union membuat
-
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
Argumentsuntuk 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
- Jika callee tidak membutuhkannya, caller bisa menghindari alokasi objek
ZeroArgumentsdiperlukan untuk membedakannya dari(Arguments*)nullptr- Sebelumnya,
(Arguments*)nullptrdigunakan dengan makna pemanggilan getter, dan logika itu tetap dipertahankan - Kini
ZeroArgumentsberarti pemanggilan fungsi tanpa argumen
- Sebelumnya,
- 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::getArgsebagai helper ekstraksi argumen, dan di sini ditambahkan overload argumen terspesialisasi - Perubahan pada kode native yang menggunakan argumen relatif merupakan modifikasi yang lugas
- Untuk masing-masing
- 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
- Semua fungsi built-in di Zef menerima 1 atau 2 argumen, dan pada implementasi native tidak perlu mengalokasikan objek
Mengakali patologi Fil-C dan spesialisasi yang lebih rinci
-
Optimisasi #14:
slow pathValue yang ditingkatkan- Mendapatkan peningkatan kecepatan besar lewat pengakalan patologi Fil-C lainnya
- Sebelum perubahan,
slow pathout-of-line milikValueadalah fungsi anggotaValue, dan memerlukan argumen implisitconst Value* - Dalam struktur ini, caller harus mengalokasikan
Valuedi stack - Di Fil-C++, semua alokasi stack adalah alokasi heap
- Karena itu, kode yang memanggil slow path akan mengalokasikan
Valuedi heap
- Karena itu, kode yang memanggil slow path akan mengalokasikan
- Setelah perubahan, metode-metode ini diubah menjadi
static, danValuedikirim 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<>, danValue::callRMW<>bergantung pada cara memeriksa apakah receiver adalahintataudouble - Cara ini hanya berlaku untuk operator yang dikenali parser
- Tidak berlaku untuk bentuk seperti
value.sqrt
- Tidak berlaku untuk bentuk seperti
- Dengan perubahan ini,
Dotbisa dispesialisasi untukvalue.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
toStringdengan cara yang hampir sama seperti optimisasi sebelumnya - Perubahan ini juga mencakup logika pengurangan jumlah alokasi saat mengubah
intmenjadi 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
- Menerapkan spesialisasi
-
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,3secara rekursif setiap saat - Perubahan kali ini membuat node
ArrayLiteraldapat 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
- Kode seperti
-
Optimisasi #19: peningkatan
Value::callOperator- Menerapkan optimisasi yang sama seperti sebelumnya—yang memberi peningkatan kecepatan dengan tidak mengirim
Valuesebagai referensi—keslow pathcallOperatorjuga - 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
- Menerapkan optimisasi yang sama seperti sebelumnya—yang memberi peningkatan kecepatan dengan tidak mengirim
-
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
ZASSERTkhusus Fil-C- Strukturnya selalu menjalankan assert
- Setelah perubahan, digunakan makro internal
ASSERT- Assert hanya dijalankan jika
ASSERTS_ENABLEDdisetel
- Assert hanya dijalankan jika
- 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
- Tidak sound karena pemanggilan GC Fil-C++ yang ada berubah menjadi pemanggilan
- Disebut suboptimal karena allocator GC yang sebenarnya lebih cepat daripada
callocmilik 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,geomeanuntuk tiap interpreter -
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 Baseline
nbody2.9573splay13.0286richards1.9251deltablue5.9997geomean4.5927
-
Zef Perubahan #1: Operator Langsung
nbody2.1891splay12.0233richards1.6935deltablue5.2331geomean3.9076
-
Zef Perubahan #2: RMW Langsung
nbody2.0130splay11.9987richards1.6367deltablue5.0994geomean3.7677
-
Zef Perubahan #3: Hindari IntObject
nbody1.9922splay11.8824richards1.6220deltablue5.0646geomean3.7339
-
Zef Perubahan #4: Symbol
nbody1.5782splay9.9577richards1.4116deltablue4.4593geomean3.1533
-
Zef Perubahan #5: Value Inline
nbody1.4982splay9.7723richards1.3890deltablue4.3536geomean3.0671
-
Zef Perubahan #6: Model Objek dan Inline Cache
nbody0.3884splay3.3609richards0.2321deltablue0.6805geomean0.6736
-
Zef Perubahan #7: Argumen
nbody0.3160splay2.6890richards0.1653deltablue0.4738geomean0.5077
-
Zef Perubahan #8: Getter
nbody0.2988splay2.6919richards0.1564deltablue0.4260geomean0.4809
-
Zef Perubahan #9: Setter
nbody0.2850splay2.6690richards0.1514deltablue0.4072geomean0.4651
-
Zef Perubahan #10:
callMethodinlinenbody0.2533splay2.6711richards0.1513deltablue0.4032geomean0.4506
-
Zef Perubahan #11: Hashtable
nbody0.1796splay2.6528richards0.1379deltablue0.3551geomean0.3906
-
Zef Perubahan #12: Hindari
std::optionalnbody0.1689splay2.6563richards0.1379deltablue0.3518geomean0.3839
-
Zef Perubahan #13: Argumen Terspesialisasi
nbody0.1610splay2.5823richards0.1350deltablue0.3372geomean0.3707
-
Zef Perubahan #14: Slow Path Value yang Ditingkatkan
nbody0.1348splay2.5062richards0.1241deltablue0.3076geomean0.3367
-
Zef Perubahan #15:
DotSetRMW::evaluateyang Dideduplikasinbody0.1342splay2.5047richards0.1256deltablue0.3079geomean0.3375
-
Zef Perubahan #16:
sqrtcepatnbody0.1274splay2.5045richards0.1251deltablue0.3060geomean0.3322
-
Zef Perubahan #17:
toStringcepatnbody0.1282splay2.2664richards0.1275deltablue0.2964geomean0.3235
-
Zef Perubahan #18: Spesialisasi Literal Array
nbody0.1295splay1.6661richards0.1250deltablue0.2979geomean0.2992
-
Zef Perubahan #19: Optimisasi
callOperatorpada Valuenbody0.1208splay1.6698richards0.1143deltablue0.2713geomean0.2810
-
Zef Perubahan #20: Konfigurasi C++ yang Lebih Baik
nbody0.1186splay1.6521richards0.1127deltablue0.2635geomean0.2760
-
Zef Perubahan #21: Tanpa Assert
nbody0.1194splay1.6504richards0.1127deltablue0.2619geomean0.2759
-
Zef dalam Yolo-C++
nbody0.0233splay0.3992richards0.0309deltablue0.0784geomean0.0686
1 komentar
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?
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
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
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
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 generikPada 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
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
awaitpertama, inlining pun runtuh besar-besaran, dan alasan memakai Cow sendiri jadi hilangAkhirnya 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 siniSatu 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
isIntitu sendiri bisa dihilangkanDugaan 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
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
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
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
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