2 điểm bởi GN⁺ 8 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Ngay cả trình thông dịch duyệt AST trực tiếp cũng có thể cải thiện hiệu năng rất lớn chỉ với biểu diễn giá trị, inline cache, mô hình đối tượng, watchpoint và việc lặp lại các tối ưu hóa chi tiết
  • Mốc cơ sở Zef gần như không hề cân nhắc hiệu năng thì chậm hơn CPython 3.10 35 lần, chậm hơn Lua 5.4.7 80 lần và chậm hơn QuickJS-ng 0.14.0 23 lần, nhưng sau 21 bước tối ưu hóa đã đạt tăng tốc 16,646 lần
  • Bước nhảy lớn nhất đến từ việc thiết kế lại mô hình đối tượng kết hợp với inline cache, sau đó tiếp tục cải thiện 4,55 lần nhờ truy cập dựa trên Storage và Offsets, chuyên biệt hóa AST được lưu cache và áp dụng watchpoint để giám sát ghi đè tên
  • Các cải thiện bổ sung được cộng dồn gồm loại bỏ dispatch dựa trên chuỗi, đưa vào Symbol, thay đổi cấu trúc truyền đối số, chuyên biệt hóa getter và setter, đường tắt cho hash table, cùng chuyên biệt hóa array literal và sqrt·toString
  • Nếu tính cả bản chuyển sang Yolo-C++, hệ thống ghi nhận nhanh hơn 66,962 lần so với mốc cơ sở, nhanh hơn CPython 3.10 1,889 lần và nhanh hơn QuickJS-ng 0.14.0 2,968 lần, nhưng vì không có giải phóng bộ nhớ nên không phù hợp với workload chạy dài hạn

Giới thiệu và phương pháp đánh giá

  • Mục tiêu tối ưu hóa là trình thông dịch duyệt AST trực tiếp, với đích đến là đưa ngôn ngữ động Zef viết cho vui lên mức có thể cạnh tranh với Lua, QuickJS và CPython
    • Thay vì tinh chỉnh vi mô cho JIT compiler hay GC đã trưởng thành, bài viết tập trung vào các tối ưu hóa có thể áp dụng ngay cả khi xuất phát từ một nền tảng chưa có gì
    • Các kỹ thuật được bàn tới là biểu diễn giá trị, inline caching, mô hình đối tượng, watchpointviệc lặp lại các tối ưu hóa hợp lý
  • Chỉ với các kỹ thuật trong bài, vẫn đạt được cải thiện hiệu năng lớn ngay cả khi không có SSA, GC, bytecode hay mã máy
    • Theo phạm vi bài viết là tăng tốc 16 lần
    • Nếu tính cả bản chuyển Yolo-C++ chưa hoàn thiện thì là tăng tốc 67 lần
  • Đánh giá hiệu năng sử dụng bộ benchmark ScriptBench1
    • Các benchmark gồm Richards OS scheduler, DeltaBlue constraint solver, N-Body mô phỏng vật lý và Splay kiểm thử cây nhị phân
    • Sử dụng các bản port sẵn có cho JavaScript, Python và Lua
    • Các bản port Python và Lua của Splay được tạo bằng Claude
  • Môi trường thử nghiệm là Ubuntu 22.04.5, Intel Core Ultra 5 135U, 32GB RAM, Fil-C++ 0.677
    • Lua 5.4.7 được biên dịch bằng GCC 11.4.0
    • QuickJS-ng 0.14.0 dùng binary phát hành trên GitHub releases
    • CPython 3.10 dùng phiên bản mặc định do Ubuntu cung cấp
  • Tất cả thí nghiệm đều dùng giá trị trung bình của 30 lần chạy được xáo trộn ngẫu nhiên
  • Phần lớn so sánh được thực hiện giữa trình thông dịch Zef biên dịch bằng Fil-C++các trình thông dịch khác được build bằng trình biên dịch Yolo-C

Trình thông dịch Zef ban đầu

  • Được viết gần như không hề cân nhắc hiệu năng, và tác giả nêu rõ chỉ có hai lựa chọn được đưa ra với ý thức về hiệu năng
  • Biểu diễn giá trị

    • Sử dụng tagged value 64 bit
      • Các giá trị có thể chứa là double, số nguyên 32 bit, Object*
    • double được biểu diễn bằng cách dùng offset 0x1000000000000
      • Được giới thiệu là kỹ thuật học từ JavaScriptCore
      • Trong tài liệu, kỹ thuật này được gọi là NuN tagging
    • Số nguyên và con trỏ dùng biểu diễn native
      • Dựa trên giả định rằng giá trị con trỏ không nhỏ hơn 0x100000000
      • Tác giả trực tiếp nói đây là một lựa chọn nguy hiểm
      • Đồng thời nhắc rằng một phương án khác là gắn thẻ bit cao 0xffff000000000000 cho số nguyên
    • Cách biểu diễn này cho phép triển khai đường nhanh dựa trên kiểm tra bit trong phép toán số học
    • Lợi ích quan trọng hơn là tránh cấp phát heap cho số
    • Khi tạo một trình thông dịch mới, việc chọn đúng biểu diễn giá trị cơ bản ngay từ đầu là rất quan trọng, vì sau này thay đổi sẽ cực kỳ khó
    • Là điểm khởi đầu để hiện thực ngôn ngữ kiểu động, bài viết gợi ý tagged value 32 bit hoặc 64 bit
  • Lựa chọn ngôn ngữ triển khai

    • Chọn họ ngôn ngữ C++ vì có thể chứa đủ các tối ưu hóa cần thiết
    • Tác giả nêu rõ sẽ không chọn Java vì giới hạn trần tối ưu hóa ở mức thấp
    • Tác giả cũng nói sẽ không chọn Rust do biểu diễn heap cho ngôn ngữ có GC cần trạng thái khả biến toàn cụctham chiếu vòng
      • Dù vậy, tác giả cũng nhắc rằng vẫn có thể dùng Rust cho một phần hoặc toàn bộ nếu chấp nhận cấu hình đa ngôn ngữ hoặc cho phép nhiều mã unsafe
  • Những lựa chọn sai lầm từ góc nhìn kỹ thuật hiệu năng

    • Dùng Fil-C++
      • Giúp phát triển nhanh và cung cấp GC miễn phí
      • Báo các vi phạm an toàn bộ nhớ bằng thông tin chẩn đoán và stack trace
      • Không có undefined behavior
      • Cái giá về hiệu năng thường là khoảng 4 lần
    • Trình thông dịch đi bộ AST đệ quy
      • Cấu trúc với các phương thức virtual Node::evaluate bị override ở nhiều nơi
    • Lạm dụng chuỗi
      • Nút AST Get lưu std::string mô tả tên biến
      • Mỗi lần truy cập biến đều dùng chuỗi đó
    • Lạm dụng hash table
      • Khi thực thi Get, hệ thống tra cứu std::unordered_map bằng khóa chuỗi
    • Tìm scope dựa trên chuỗi gọi đệ quy
      • Cho phép hầu như mọi kiểu lồng nhau và closure
      • Trong các cấu trúc lồng nhau như lớp A trong hàm F, hàm G trong lớp B, thì phương thức của A có thể nhìn thấy field của A, biến cục bộ của F, field của B, biến cục bộ của G
      • Bản triển khai ban đầu xử lý điều này bằng các hàm đệ quy C++ truy vấn những đối tượng scope khác nhau
  • Đặc tính của bản triển khai ban đầu

    • Dù có những lựa chọn sai, vẫn có thể hiện thực một trình thông dịch ngôn ngữ khá phức tạp với ít mã nguồn
    • Mô-đun lớn nhất là parser
    • Phần còn lại nhìn chung đơn giản và rõ ràng
  • Hiệu năng ban đầu

    • Trình thông dịch ban đầu chậm hơn CPython 3.10 35 lần
    • Chậm hơn Lua 5.4.7 80 lần

      • Chậm hơn QuickJS-ng 0.14.0 23 lần

Bảng tiến trình tối ưu hóa tổng thể

  • Bảng tổng hợp mức thay đổi hiệu năng từ Zef Baseline tới Zef Change #21: No Asserts, và Zef in Yolo-C++
    • Các cột so sánh là vs Zef Baseline, vs Python 3.10, vs Lua 5.4.7, vs QuickJS-ng 0.14.0
  • Theo hàng cuối, Zef Change #21: No Asserts nhanh hơn mốc cơ sở 16,646 lần
    • Vẫn chậm hơn Python 3.10 2,13 lần

    • Vẫn chậm hơn Lua 5.4.7 4,781 lần

      • Vẫn chậm hơn QuickJS-ng 0.14.0 1,355 lần
  • Zef in Yolo-C++** nhanh hơn mốc cơ sở** 66,962 lần

    • Nhanh hơn Python 3.10 1,889 lần

    • Chậm hơn Lua 5.4.7 1,189 lần

      • Nhanh hơn QuickJS-ng 0.14.0 2,968 lần

Giai đoạn tối ưu hóa ban đầu

  • Tối ưu hóa #1: Gọi trực tiếp toán tử

    • Trình phân tích cú pháp không còn tạo toán tử dưới dạng nút DotCall mang tên toán tử nữa, mà tạo nút AST riêng cho từng toán tử
    • Trong Zef, a + ba.add(b) là như nhau
      • Trước đây, a + b được phân tích thành DotCall(a, "add") và đối số b
      • Mỗi phép toán số học đều phát sinh tra cứu chuỗi tên phương thức toán tử
      • DotCall truyền chuỗi vào Value::callMethod
      • Value::callMethod thực hiện nhiều lần so sánh chuỗi
    • Sau thay đổi, trình phân tích cú pháp tạo các nút Binary<>, Unary<>
      • Tận dụng template và lambda để cung cấp các Node::evaluate override khác nhau cho từng toán tử
      • Mỗi nút gọi trực tiếp đường đi nhanh Value của toán tử tương ứng
      • Ví dụ, a + b gọi Binary<lambda for add>::evaluate rồi gọi Value::add
    • Hiệu quả hiệu năng là cải thiện 17,5%
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 30 lần
      • Chậm hơn Lua 5.4.7 67 lần
      • Chậm hơn QuickJS-ng 0.14.0 19 lần
  • Tối ưu hóa #2: Gọi trực tiếp toán tử RMW

    • Các toán tử thông thường đã nhanh hơn, nhưng dạng RMW như a += b vẫn dùng dispatch dựa trên chuỗi
    • Đã thay đổi để trình phân tích cú pháp tạo nút riêng cho từng trường hợp RMW
    • Trình phân tích cú pháp yêu cầu nút LValue tự thay thế thành RMW thông qua lời gọi ảo makeRMW
    • Các LValue có thể chuyển thành RMW là Get, Dot, Subscript
      • Get tương ứng với đọc biến id
      • Dot tương ứng với expr.id
      • Subscript tương ứng với expr[index]
    • Mỗi lời gọi ảo sử dụng macro SPECIALIZE_NEW_RMW
      • SetRMWid += value
      • DotSetRMWexpr.id += value
      • SubscriptRMWexpr[index] += value
    • Việc chuyên biệt hóa toán tử trong thay đổi #1 dùng dispatch bằng lambda
    • RMW dùng enum
      • Được chọn vì phải xử lý cả ba đường đi get, dot, subscript và truyền enum qua nhiều vị trí
      • Cuối cùng, hàm template Value::callRMW<> thực hiện dispatch gọi toán tử RMW thực tế
    • Hiệu quả hiệu năng là cải thiện 3,7%
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 29 lần
      • Chậm hơn Lua 5.4.7 65 lần
      • Chậm hơn QuickJS-ng 0.14.0 18,5 lần
      • Nhanh hơn 1,22 lần so với điểm xuất phát
  • Tối ưu hóa #3: Tránh kiểm tra IntObject

    • Điểm nghẽn là đường đi nhanh của Value dùng isInt(), còn isIntSlow() bên trong thực hiện lời gọi ảo Object::isInt()
    • Biểu diễn giá trị ban đầu có bốn trường hợp
      • tagged int32
      • tagged double
      • IntObject dành cho int64 không thể biểu diễn bằng int32
      • mọi đối tượng khác
    • Ngay cả với trường hợp IntObject, việc dispatch phương thức số nguyên vẫn do Value đảm nhiệm
      • Nhằm giữ toàn bộ phần triển khai phép toán số học ở một nơi, tức là Value
    • Sau tối ưu hóa, đường đi nhanh của Value chỉ xét int32 và double
      • Logic xử lý IntObject được chuyển vào chính IntObject
      • Tránh được lời gọi isInt() vốn phát sinh ở mỗi lần dispatch phương thức
    • Hiệu quả hiệu năng là cải thiện 1%
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 29 lần
      • Chậm hơn Lua 5.4.7 65 lần
      • Chậm hơn QuickJS-ng 0.14.0 18 lần
      • Nhanh hơn 1,23 lần so với điểm xuất phát
  • Tối ưu hóa #4: Symbol

    • Trình thông dịch ban đầu dùng std::string ở gần như mọi nơi
    • Các vị trí dùng chuỗi có chi phí lớn là Context::get, Context::set, Context::callFunction, Value::callMethod, Value::dot, Value::setDot, Value::callOperator<>, và nhóm Object::callMethod
    • Với cấu trúc này, đây không phải tra cứu hashtable đơn thuần mà là tra cứu hashtable với khóa chuỗi, nên trong lúc chạy phải lặp đi lặp lại việc băm và so sánh chuỗi
    • Tối ưu hóa thay việc tra cứu dựa trên chuỗi bằng con trỏ đối tượng Symbol hash-consed
    • Thêm lớp Symbol mới
      • Được triển khai trong symbol.h, symbol.cpp
      • Có thể chuyển đổi qua lại giữa Symbol và chuỗi
      • Khi đổi chuỗi thành Symbol, thực hiện hash consing bằng hashtable toàn cục
      • Kết quả là có thể xác định cùng một symbol chỉ bằng so sánh tính đồng nhất của con trỏ Symbol*
    • Dùng symbol được chuẩn bị sẵn thay cho literal chuỗi
      • Ví dụ, dùng Symbol::subscript thay cho "subscript"
    • Nhiều chữ ký hàm đã được đổi từ const std::string& sang Symbol*
    • Hiệu quả hiệu năng là cải thiện 18%
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 24 lần
      • Chậm hơn Lua 5.4.7 54 lần
      • Chậm hơn QuickJS-ng 0.14.0 15 lần
      • Nhanh hơn 1,46 lần so với điểm xuất phát
  • Tối ưu hóa #5: Inline hóa Value

    • Mấu chốt là cho phép inline hóa các hàm quan trọng
    • Gần như mọi thay đổi đều xoay quanh việc đưa vào header mới valueinlines.h
    • Lý do tách thành header riêng với value.h là vì nó dùng các header phải include value.h
    • Hiệu quả hiệu năng là cải thiện 2,8%
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 24 lần
      • Chậm hơn Lua 5.4.7 53 lần
      • Chậm hơn QuickJS-ng 0.14.0 15 lần
      • Nhanh hơn 1,5 lần so với điểm xuất phát

Thiết kế lại mô hình đối tượng và cấu trúc cache

  • Tối ưu hóa #6: mô hình đối tượng, inline cache, watchpoint

    • Tái cấu trúc quy mô lớn cách hoạt động của Object, ClassObject, Context để giảm chi phí cấp phát đối tượng và tránh tra cứu bảng băm khi truy cập
    • Thay đổi này kết hợp ba tính năng: mô hình đối tượng, inline cache, watchpoint
  • Mô hình đối tượng

    • Trước đây, mỗi lexical scope đều cấp phát một đối tượng Context
      • Mỗi Context giữ một bảng băm chứa các biến của scope đó
    • Đối tượng có cấu trúc phức tạp hơn
      • Mỗi đối tượng giữ một bảng băm ánh xạ các lớp mà nó là instance tới Context
    • Lý do cần cấu trúc này là vì kế thừa và nested scope
      • Khi Bar kế thừa Foo, BarFoo close over các scope khác nhau
      • Chúng cũng có thể có các trường private khác nhau nhưng cùng tên
    • Cấu trúc mới đưa vào khái niệm Storage
      • Dữ liệu được lưu theo các offset
      • offset do một Context quyết định
    • Context vẫn tồn tại, nhưng không được tạo khi tạo đối tượng hay scope mà được tạo trước trong pass resolve của AST
    • Khi tạo đối tượng hoặc scope thực tế, chỉ cấp phát Storage theo kích thước mà Context đã tính toán
  • Inline cache

    • Là kỹ thuật ghi nhớ kiểu động của expr được thấy lần gần nhất và offset cuối cùngname được phân giải tại một vị trí mã như expr.name
    • Đây là kỹ thuật kinh điển thường được giải thích trong ngữ cảnh JIT, nhưng ở đây được áp dụng cho trình thông dịch
    • Thông tin được ghi nhớ được hiện thực bằng cách placement construct các AST node chuyên biệt ngay trên AST node thông thường
  • Các thành phần của inline cache

    • CacheRecipe
      • Theo dõi một truy cập cụ thể đã làm gì và liệu có thể cache hay không
    • Chèn các lời gọi CacheRecipe khắp Context, ClassObject, Package
      • Thu thập thông tin về quá trình truy cập
    • Các hàm đánh giá AST như Dot::evaluate truyền CacheRecipe thu được từ phép toán đa hình mà chúng thực hiện, cùng với this, vào constructCache<>
    • constructCache
      • Biên dịch chuyên biệt AST node mới dựa trên CacheRecipe
      • Dùng cơ chế template để tạo ra nhiều AST node chuyên biệt khác nhau
      • Nếu là truy cập biến cục bộ thì load trực tiếp từ storage được truyền vào
      • Kiểm tra class check để xác nhận có đúng lớp được thấy lần gần nhất hay không
      • Sau đó gọi trực tiếp hàm được thấy lần gần nhất
      • Nếu cần thì kết hợp chain stepwatchpoint
    • Mỗi AST node được cache đều giữ một cached variant
      • Trước tiên thử gọi nhanh qua đối tượng cache
      • Kiểu của đối tượng cache do constructCache<> quyết định
  • Watchpoint

    • Đưa ra ví dụ lexical scope có biến x, bên trong có lớp Foo, và phương thức của Foo truy cập x
    • Nếu bên trong Foo không có hàm hay biến tên x, có vẻ như nó có thể đọc trực tiếp x ở scope bên ngoài
    • Tuy nhiên, lớp con có thể thêm getter x
    • Khi đó kết quả truy cập phải là getter chứ không còn là x bên ngoài
    • Để xử lý khả năng thay đổi này, inline cache thiết lập Watchpoint tại runtime
    • Trong ví dụ, watchpoint được dùng để theo dõi liệu tên này có bị override hay không
  • Lý do triển khai đồng thời cả ba tính năng

    • Chỉ riêng mô hình đối tượng mới thì khó mang lại cải thiện đáng kể nếu inline cache không hoạt động tốt
    • Inline cache cũng khó xử lý an toàn nhiều điều kiện cache nếu không có watchpoint, nên lợi ích thực tế nhỏ
    • Mô hình đối tượng mới và watchpoint cần hoạt động tốt cùng nhau
  • Quá trình triển khai và những phần khó

    • Ban đầu bắt đầu bằng việc viết một phiên bản CacheRecipe đơn giản, cùng với thiết kế Storageoffset đã khá gần dạng cuối cùng
    • Một trong những việc khó nhất là thay đổi cách hiện thực intrinsic class
    • Ví dụ về mảng
      • Trước đây, ArrayObject::tryCallMethod hiện thực toàn bộ phương thức bằng cách chặn lời gọi ảo Object::tryCallMethod
      • Trong mô hình đối tượng mới, Object không có vtable và cũng không có phương thức ảo
      • Thay vào đó, Object::tryCallMethod ủy quyền sang object->classObject()->tryCallMethod(object, ...)
      • Vì vậy, để cung cấp các phương thức của Array, cần tạo chính lớp dành cho Array có chứa các phương thức đó
    • Kết quả là phần lớn chức năng intrinsic được chuyển từ cấu trúc rải rác khắp quá trình hiện thực sang tập trung quanh makerootcontext.cpp
    • Đây được xem là kết quả tích cực vì inline cache vẫn được áp dụng nguyên vẹn cho cả các hàm native/intrinsic của đối tượng
    • Hiệu quả hiệu năng là tăng 4,55 lần
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 khoảng 5,2 lần
      • Chậm hơn Lua 5.4.7 khoảng 11,7 lần
      • Chậm hơn QuickJS-ng 0.14.0 khoảng 3,3 lần
      • Nhanh hơn 6,8 lần so với điểm khởi đầu
      • Tác giả đánh giá rằng mức thua kém của Fil-C++ so với các trình thông dịch khác nhìn chung đã thu hẹp xuống mức chi phí riêng của Fil-C

Tối ưu hóa đường dẫn gọi và truy cập

  • Tối ưu hóa #7: Cải thiện cấu trúc truyền đối số

    • Trước thay đổi, trình thông dịch Zef truyền đối số hàm dưới dạng const std::optional<std::vector<Value>>&
    • Lý do cần optional là vì trong một số trường hợp biên, cần phân biệt hai dạng sau
      • o.getter
      • o.function()
    • Trong Zef, nhìn chung cả hai đều là lời gọi hàm, nhưng ngoại lệ là có đoạn mã sau
      • o.NestedClass
      • o.NestedClass()
    • Dạng đầu trả về chính đối tượng NestedClass
    • Dạng sau là tạo instance
    • Vì vậy cần phân biệt giữa lời gọi hàm không có đối sốtrường hợp gọi kiểu getter mà mảng đối số rỗng
    • Tuy nhiên cấu trúc cũ kém hiệu quả
      • Bên gọi phải cấp phát vector
      • Bên được gọi lại cấp phát tiếp một arguments scope là bản sao của vector đó
    • Thay đổi là đưa vào kiểu Arguments
      • Hình dạng của nó giống hệt arguments scope mà bên được gọi từng tạo ra
      • Giờ đây bên gọi cấp phát trực tiếp theo chính dạng đó
    • Trong Yolo-C++ cũng giảm số lần cấp phát nhờ loại bỏ malloc của vùng lưu trữ nền vector
    • Trong Fil-C++ thì bản thân std::optional cũng cấp phát trên heap
      • Ngay cả khi không có std::optional, việc truyền const std::vector<>& cũng vẫn cấp phát
      • Những gì lẽ ra cấp phát trên stack lại được chỉ định là cấp phát trên heap
      • Cũng có nhắc tới việc phía bên gọi không định sẵn kích thước vector nên bị tái cấp phát nhiều lần
    • Phần lớn thay đổi là công việc thay chữ ký hàm thành Arguments*
    • Hiệu quả hiệu năng là tăng 1,33 lần
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 3,9 lần
      • Chậm hơn Lua 5.4.7 8,8 lần
      • Chậm hơn QuickJS-ng 0.14.0 2,5 lần
      • Nhanh hơn 9,05 lần so với điểm xuất phát
  • Tối ưu hóa #8: Chuyên biệt hóa getter

    • Zef, tương tự Ruby, có trường instance mặc định là private
    • Ví dụ class Foo { my f fn (inF) f = inF }
      • Lưu giá trị nhận từ constructor vào biến cục bộ f chỉ hiển thị với instance
    • Ngay cả các instance cùng kiểu cũng không thể truy cập f của đối tượng khác
      • Ví dụ fn nope(o) o.f
      • println(Foo(42).nope(Foo(666)))
      • o.f bên trong nope không thể truy cập f của o
    • Lý do là vì trường hoạt động theo cách nó xuất hiện trong chuỗi scope của thành viên lớp
      • o.f không phải là đọc trường mà là yêu cầu gọi phương thức tên f
    • Vì thế mẫu sau xuất hiện thường xuyên
      • my f
      • fn f f
      • Tức là một phương thức tên f trả về biến cục bộ f
    • Có cú pháp ngắn hơn là readable f
      • Dạng rút gọn của my ffn f f
    • Nhiều lời gọi phương thức trên thực tế là lời gọi getter
    • Để mọi getter đều hoạt động bằng cách đánh giá AST là lãng phí
    • Tối ưu hóa ở đây là chuyên biệt hóa getter
      • Trọng tâm là UserFunction
      • Dùng phương thức mới Node::inferGetter để suy luận xem thân hàm có phải getter đơn giản hay không
    • Quy tắc suy luận
      • Block::inferGetter sẽ suy luận mình là getter nếu mọi thứ nó chứa đều có thể suy luận thành getter
      • Get::inferGetter suy luận chính nó là getter và trả về offset cần nạp
      • Context::tryGetFieldOffsets chỉ trả về Offsets không rỗng khi trường đó chắc chắn tồn tại trong lexical scope nơi getter sẽ chạy
      • UserFunction sẽ resolve thành một lớp con Function chuyên biệt chỉ đọc trực tiếp tại offset đã biết nếu thân hàm có thể được suy luận là getter
    • Hiệu quả hiệu năng là tăng 5,6%
      • Ở thời điểm này, hiệu năng chậm hơn CPython 3.10 3,7 lần
      • Chậm hơn Lua 5.4.7 8,3 lần
      • Chậm hơn QuickJS-ng 0.14.0 2,4 lần
      • Nhanh hơn 9,55 lần so với điểm xuất phát
  • Tối ưu hóa #9: Chuyên biệt hóa setter

    • Trong suy luận setter, cần pattern matching mẫu fn set_fieldName(newValue) fieldName = newValue
    • Ở giai đoạn suy luận của UserFunction, cần truyền tên tham số của setter
    • Ở giai đoạn suy luận của Set, cần kiểm tra đó không phải là ghi vào ClassObject, đồng thời cũng cần kiểm tra tham số setter có được dùng làm nguồn của phép gán hay không
    • Hiệu quả hiệu năng là tăng 3,4%
      • Tại thời điểm này, Zef chậm hơn CPython 3.10 3,6 lần
      • Chậm hơn Lua 5.4.7 8 lần
      • Chậm hơn QuickJS-ng 0.14.0 2,3 lần
      • Nhanh hơn 9,87 lần so với điểm xuất phát
  • Tối ưu hóa #10: Inline hóa callMethod

    • Inline một hàm quan trọng chỉ bằng một thay đổi một dòng
    • Hiệu quả hiệu năng là tăng 3,2%
      • Tại thời điểm này, Zef chậm hơn CPython 3.10 3,5 lần
      • Chậm hơn Lua 5.4.7 7,8 lần
      • Chậm hơn QuickJS-ng 0.14.0 2,2 lần
      • Nhanh hơn 10,2 lần so với điểm xuất phát
  • Tối ưu hóa #11: Bảng băm

    • Khi xảy ra inline cache miss trong lời gọi phương thức, phải đi xuống ClassObject::tryCallMethodClassObject::TryCallMethodDirect, mà cả hai đường này đều lớn và phức tạp
    • Chi phí tìm kiếm trước đây là O(độ sâu hệ phân cấp), tỷ lệ với độ sâu phân cấp
      • Ở mỗi lớp trong hệ phân cấp đều phải tra bảng băm để kiểm tra xem lời gọi có được phân giải thành hàm thành viên hay không
      • Ở mỗi lớp trong hệ phân cấp cũng phải tra bảng băm để kiểm tra xem lời gọi có được phân giải thành lớp lồng nhau hay không
    • Thay đổi mới đưa vào bảng băm toàn cục dùng receiver class và symbol làm khóa
      • Chỉ với một lần tra cứu là trả về trực tiếp callee
      • Trong classobject.h, trước khi đi xuống toàn bộ tryCallMethodSlow, sẽ tra bảng toàn cục này trước
      • Trong classobject.cpp, kết quả tra cứu thành công sẽ được ghi vào bảng toàn cục
      • Bản thân bảng băm toàn cục là một cài đặt tương đối đơn giản
    • Hiệu quả hiệu năng là tăng 15%
      • Tại thời điểm này, Zef chậm hơn CPython 3.10 3 lần
      • Chậm hơn Lua 5.4.7 6,8 lần
      • Chậm hơn QuickJS-ng 0.14.0 1,9 lần
      • Nhanh hơn 11,8 lần so với điểm xuất phát
  • Tối ưu hóa #12: Tránh std::optional

    • Trong Fil-C++, do bệnh lý trình biên dịch liên quan đến union, std::optional cần cấp phát trên heap
    • Thông thường LLVM xử lý kiểu truy cập bộ nhớ của union khá lỏng, nhưng điều này lại xung đột với invisicaps
      • Có trường hợp con trỏ bên trong union mất capability theo cách khó dự đoán từ góc nhìn của lập trình viên
      • Kết quả là trong Fil-C có thể xảy ra panic dereference đối tượng với null capability ngay cả khi lập trình viên không mắc lỗi
    • Để giảm nhẹ điều này, trình biên dịch Fil-C++ chèn intrinsics để LLVM hành xử bảo thủ hơn khi xử lý biến cục bộ kiểu union
    • Sau đó pass FilPizlonator thực hiện escape analysis riêng để cố gắng cho phép biến cục bộ kiểu union được gán vào thanh ghi
      • Tuy vậy, phân tích này không hoàn chỉnh bằng phân tích SROA thông thường của LLVM
    • Kết quả là trong Fil-C++, việc truyền một lớp chứa union như std::optional thường dẫn tới cấp phát bộ nhớ
    • Thay đổi lần này là tránh các đường mã trong hot path dẫn tới std::optional
    • Hiệu quả hiệu năng là tăng 1,7%
      • Tại thời điểm này, Zef chậm hơn CPython 3.10 3 lần
      • Chậm hơn Lua 5.4.7 6,65 lần
      • Chậm hơn QuickJS-ng 0.14.0 1,9 lần
  • Nhanh hơn 12 lần so với điểm xuất phát

  • Tối ưu hóa #13: đối số chuyên biệt hóa

    • Mọi hàm built-in của Zef đều nhận 1 hoặc 2 đối số, và trong triển khai native thì không cần cấp phát đối tượng Arguments để chứa chúng
    • Setter cũng luôn nhận một đối số, và khi suy luận setter được thực hiện thì triển khai setter chuyên biệt hóa cũng chỉ cần nhận trực tiếp đối số giá trị mà không cần đối tượng Arguments
    • Với thay đổi này, đã đưa vào các kiểu đối số chuyên biệt hóa ZeroArguments, OneArgument, TwoArguments
      • Khi callee không cần, caller có thể tránh cấp phát đối tượng Arguments
    • Cần có ZeroArguments để phân biệt với (Arguments*)nullptr
      • Trước đây, (Arguments*)nullptr được dùng với nghĩa là gọi getter, và logic đó được giữ nguyên
      • Giờ đây ZeroArguments mang nghĩa là gọi hàm không có đối số
    • Nhiều thay đổi chủ yếu là công việc template hóa các hàm nhận đối số
      • Thực hiện khởi tạo tường minh cho từng trường hợp ZeroArguments, OneArgument, TwoArguments, Arguments*
      • Phần lớn mã hiện có dùng Value::getArg làm helper trích xuất đối số, và ở đây đã bổ sung overload đối số chuyên biệt hóa
      • Việc sửa mã native có sử dụng đối số tương đối là những chỉnh sửa thẳng thắn, trực diện
    • Hiệu quả về hiệu năng là cải thiện 3,8%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 khoảng 2,9 lần
      • Chậm hơn Lua 5.4.7 khoảng 6,4 lần
      • Chậm hơn QuickJS-ng 0.14.0 khoảng 1,8 lần
      • Nhanh hơn 12,4 lần so với điểm xuất phát

Vượt qua bệnh lý Fil-C và chuyên biệt hóa chi tiết

  • Tối ưu hóa #14: cải thiện slow path của Value

    • Giành được mức tăng tốc lớn nhờ một cách né tránh bệnh lý Fil-C khác
    • Trước thay đổi, slow path out-of-line của Value là hàm thành viên của Value và cần đối số const Value* ngầm định
    • Với cấu trúc này, caller phải cấp phát Value trên stack
    • Trong Fil-C++, mọi cấp phát trên stack đều là cấp phát heap
      • Vì vậy, mã gọi slow path sẽ cấp phát Value trên heap
    • Sau thay đổi, các phương thức này được chuyển thành static và truyền Value theo giá trị
      • Kết quả là không còn cần cấp phát riêng
    • Tác động hiệu năng là cải thiện 10%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 2,6 lần
      • chậm hơn Lua 5.4.7 5,8 lần
      • chậm hơn QuickJS-ng 0.14.0 1,65 lần
      • nhanh hơn 13,6 lần so với điểm khởi đầu
  • Tối ưu hóa #15: loại bỏ trùng lặp trong DotSetRMW

    • Thực hiện loại bỏ một phần mã trùng lặp
    • Kỳ vọng rằng giảm mã máy trong các hàm template được chuyên biệt hóa bởi constructCache<> có thể mang lại lợi ích
    • Kết quả thực tế là không ảnh hưởng đến hiệu năng
  • Tối ưu hóa #16: chuyên biệt hóa sqrt

    • Inline cache điều hướng lời gọi khá tốt đến hàm mong muốn nhưng chỉ hoạt động với object
    • Với non-object, fast path của Binary<>, Unary<>, Value::callRMW<> dựa vào cách kiểm tra receiver có phải int hoặc double hay không
    • Cách này chỉ áp dụng cho toán tử mà parser nhận biết
      • Không áp dụng cho dạng như value.sqrt
    • Thay đổi lần này cho phép Dot được chuyên biệt hóa cho value.sqrt
    • Tác động hiệu năng là cải thiện 1,6%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 2,6 lần
      • chậm hơn Lua 5.4.7 5,75 lần
      • chậm hơn QuickJS-ng 0.14.0 1,6 lần
      • nhanh hơn 13,8 lần so với điểm khởi đầu
  • Tối ưu hóa #17: chuyên biệt hóa toString

    • Áp dụng chuyên biệt hóa toString gần như giống hệt tối ưu hóa trước
    • Thay đổi này bao gồm logic giảm số lần cấp phát khi chuyển int thành chuỗi
    • Tác động hiệu năng là cải thiện 2,7%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 2,5 lần
      • chậm hơn Lua 5.4.7 5,6 lần
      • chậm hơn QuickJS-ng 0.14.0 1,6 lần
      • nhanh hơn 14,2 lần so với điểm khởi đầu
  • Tối ưu hóa #18: chuyên biệt hóa literal mảng

    • Mã như my whatever = [1, 2, 3] trong Zef cần cấp phát mảng mới vì mảng có thể bị alias và mutable
    • Trước thay đổi, ở mỗi lần chạy nó lại đi xuống AST và đệ quy đánh giá 1, 2, 3 mỗi lần
    • Thay đổi lần này chuyên biệt hóa node ArrayLiteral cho trường hợp cấp phát mảng hằng
    • Tác động hiệu năng là cải thiện 8,1%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 2,3 lần
      • chậm hơn Lua 5.4.7 5,2 lần
      • chậm hơn QuickJS-ng 0.14.0 1,5 lần
      • nhanh hơn 15,35 lần so với điểm khởi đầu
  • Tối ưu hóa #19: cải thiện Value::callOperator

    • Áp dụng cùng kiểu tối ưu hóa từng mang lại tăng tốc trước đó nhờ không truyền Value theo tham chiếu, lần này cho slow path của callOperator
    • Tác động hiệu năng là cải thiện 6,5%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 2,2 lần
      • chậm hơn Lua 5.4.7 4,9 lần
      • chậm hơn QuickJS-ng 0.14.0 1,4 lần
      • nhanh hơn 16,3 lần so với điểm khởi đầu
  • Tối ưu hóa #20: tùy chọn C++ tốt hơn

    • Trong Fil-C++, các RTTI không cần thiết và libc++ hardening được vô hiệu hóa
    • Không có thay đổi nào trong chính mã C++, chỉ bao gồm thay đổi cấu hình hệ thống build
    • Tác động hiệu năng là cải thiện 1,8%
      • Ở thời điểm này, Zef chậm hơn CPython 3.10 2,1 lần
      • chậm hơn Lua 5.4.7 4,8 lần
      • chậm hơn QuickJS-ng 0.14.0 1,35 lần
      • nhanh hơn 16,6 lần so với điểm khởi đầu
  • Tối ưu hóa #21: vô hiệu hóa assert

    • Là tối ưu hóa cuối cùng, áp dụng vô hiệu hóa assertion theo mặc định
    • Mã trước đây dùng macro ZASSERT dành riêng cho Fil-C
      • Đây là cấu trúc luôn thực hiện assert
    • Sau thay đổi, dùng macro ASSERT nội bộ
      • Chỉ thực hiện assert khi ASSERTS_ENABLED được thiết lập
    • Thay đổi này cũng bao gồm các chỉnh sửa khác để mã có thể build bằng Yolo-C++
    • Trái với kỳ vọng, không có cải thiện tốc độ

Kết quả và giới hạn của Yolo-C++

  • Kết quả biên dịch mã bằng Yolo-C++ cho thấy tăng tốc 4 lần
  • Tuy nhiên, cách này không sound và suboptimal
    • Lý do không sound là vì lời gọi GC Fil-C++ hiện có bị thay thành lời gọi calloc
    • Kết quả là bộ nhớ không được giải phóng, và với workload chạy đủ lâu, trình thông dịch sẽ cạn kiệt bộ nhớ
    • Trong ScriptBench1, thời gian kiểm thử ngắn nên không xảy ra cạn bộ nhớ
  • Lý do suboptimal là vì bộ cấp phát GC thực tế nhanh hơn calloc của glibc 2.35
  • Vì vậy có đề cập rằng nếu bổ sung GC thực sự vào bản port Yolo-C++, có thể đạt mức tăng tốc còn lớn hơn 4 lần
  • Thí nghiệm này dùng GCC 11.4.0
  • Ở thời điểm này, Zef là
    • nhanh hơn CPython 3.10 1,9 lần

    • chậm hơn Lua 5.4.7 1,2 lần

    • nhanh hơn QuickJS-ng 0.14.0 3 lần

      • nhanh hơn 67 lần so với điểm khởi đầu

Dữ liệu benchmark thô

  • Đơn vị thời gian chạy benchmark là giây
  • Bảng bao gồm nbody, splay, richards, deltablue, geomean cho từng trình thông dịch
  • 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 Thay đổi #1: Toán tử trực tiếp

    • nbody 2.1891
    • splay 12.0233
    • richards 1.6935
    • deltablue 5.2331
    • geomean 3.9076
  • Zef Thay đổi #2: RMW trực tiếp

    • nbody 2.0130
    • splay 11.9987
    • richards 1.6367
    • deltablue 5.0994
    • geomean 3.7677
  • Zef Thay đổi #3: Tránh IntObject

    • nbody 1.9922
    • splay 11.8824
    • richards 1.6220
    • deltablue 5.0646
    • geomean 3.7339
  • Zef Thay đổi #4: Symbol

    • nbody 1.5782
    • splay 9.9577
    • richards 1.4116
    • deltablue 4.4593
    • geomean 3.1533
  • Zef Thay đổi #5: Inline giá trị

    • nbody 1.4982
    • splay 9.7723
    • richards 1.3890
    • deltablue 4.3536
    • geomean 3.0671
  • Zef Thay đổi #6: Mô hình đối tượng và inline cache

    • nbody 0.3884
    • splay 3.3609
    • richards 0.2321
    • deltablue 0.6805
    • geomean 0.6736
  • Zef Thay đổi #7: Đối số

    • nbody 0.3160
    • splay 2.6890
    • richards 0.1653
    • deltablue 0.4738
    • geomean 0.5077
  • Zef Thay đổi #8: Getter

    • nbody 0.2988
    • splay 2.6919
    • richards 0.1564
    • deltablue 0.4260
    • geomean 0.4809
  • Zef Thay đổi #9: Setter

    • nbody 0.2850
    • splay 2.6690
    • richards 0.1514
    • deltablue 0.4072
    • geomean 0.4651
  • Zef Thay đổi #10: callMethod inline

    • nbody 0.2533
    • splay 2.6711
    • richards 0.1513
    • deltablue 0.4032
    • geomean 0.4506
  • Zef Thay đổi #11: Hashtable

    • nbody 0.1796
    • splay 2.6528
    • richards 0.1379
    • deltablue 0.3551
    • geomean 0.3906
  • Zef Thay đổi #12: Tránh std::optional

    • nbody 0.1689
    • splay 2.6563
    • richards 0.1379
    • deltablue 0.3518
    • geomean 0.3839
  • Zef Thay đổi #13: Đối số chuyên biệt hóa

    • nbody 0.1610
    • splay 2.5823
    • richards 0.1350
    • deltablue 0.3372
    • geomean 0.3707
  • Zef Thay đổi #14: Cải thiện các slow path của Value

    • nbody 0.1348
    • splay 2.5062
    • richards 0.1241
    • deltablue 0.3076
    • geomean 0.3367
  • Zef Thay đổi #15: Hợp nhất trùng lặp DotSetRMW::evaluate

    • nbody 0.1342
    • splay 2.5047
    • richards 0.1256
    • deltablue 0.3079
    • geomean 0.3375
  • Zef Thay đổi #16: sqrt nhanh

    • nbody 0.1274
    • splay 2.5045
    • richards 0.1251
    • deltablue 0.3060
    • geomean 0.3322
  • Zef Thay đổi #17: toString nhanh

    • nbody 0.1282
    • splay 2.2664
    • richards 0.1275
    • deltablue 0.2964
    • geomean 0.3235
  • Zef Thay đổi #18: Chuyên biệt hóa literal mảng

    • nbody 0.1295
    • splay 1.6661
    • richards 0.1250
    • deltablue 0.2979
    • geomean 0.2992
  • Zef Thay đổi #19: Tối ưu hóa callOperator của Value

    • nbody 0.1208
    • splay 1.6698
    • richards 0.1143
    • deltablue 0.2713
    • geomean 0.2810
  • Zef Thay đổi #20: Cấu hình C++ tốt hơn

    • nbody 0.1186
    • splay 1.6521
    • richards 0.1127
    • deltablue 0.2635
    • geomean 0.2760
  • Zef Thay đổi #21: Không dùng assert

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

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

1 bình luận

 
Ý kiến trên Hacker News
  • Theo một góc nhìn tương tự, trang này về hiệu năng của trình thông dịch Wren cũng khá thú vị
    Nếu bài của Zef tập trung vào kỹ thuật triển khai, thì phía Wren lại cho thấy thiết kế ngôn ngữ bản thân nó đóng góp cho hiệu năng như thế nào
    Đặc biệt, Wren từ bỏ dynamic object shapes để có thể dùng copy-down inheritance và khiến việc tra cứu phương thức đơn giản hơn rất nhiều, điều này có vẻ khá hay
    Cá nhân tôi thấy đây là một trade-off khá ổn. Thực tế thì việc cần thêm phương thức vào một lớp sau khi nó đã được tạo ra xảy ra thường xuyên đến mức nào chứ?

    • Tôi nghĩ tốc độ của interpreter hay JIT bị chi phối cực mạnh bởi thiết kế ngôn ngữ
      Có rất nhiều VM được tối ưu hóa mạnh cho ngôn ngữ động, nhưng tôi cảm thấy LuaJIT mạnh là vì Lua vốn dĩ là một ngôn ngữ rất nhỏ và rất hợp để tối ưu hóa
      Cũng có một vài tính năng khó tối ưu, nhưng số lượng ít nên vẫn đáng để đầu tư công sức
      Trong khi đó Python thì hoàn toàn khác. Hơi cường điệu một chút thì nó gần như được thiết kế để giảm thiểu khả năng có một JIT nhanh, và nhiều lớp tính động chồng lên nhau khiến việc tối ưu hóa trông thực sự rất khó
      Ngay cả sau từng ấy năm làm việc, việc JIT của CPython 3.15 trên x86_64 chỉ nhanh hơn interpreter mặc định khoảng 5% dường như đã cho thấy điều đó
    • Tôi thấy cách tiếp cận này khá giống với điều vẫn luôn xảy ra trong các ngôn ngữ nơi monkey patching được chấp nhận như một thông lệ, đặc biệt là Ruby
      Tất nhiên điều đó cũng khiến tôi nghĩ đến việc Ruby không phải là ngôn ngữ nổi tiếng vì đặt tốc độ lên hàng đầu
      Ngược lại, ý tưởng rằng một kiểu có một tập hàm áp dụng được ở dạng đóng cũng khiến tôi hơi băn khoăn
      Có khá nhiều ngôn ngữ cho phép định nghĩa hàm tùy ý rồi gắn nó để dùng như phương thức với cú pháp dấu chấm trên biến có kiểu tham số đầu tiên phù hợp
      Ví dụ như macro của Nim, implicit classes và type classes của Scala, extension functions của Kotlin, hay traits của Rust
    • Theo kinh nghiệm của tôi, nhìn chung nếu có thể gán kiểu tĩnh cho một biểu thức nào đó thì có thể biên dịch nó khá hiệu quả
      Các ngôn ngữ động phức tạp thường chủ động phá vỡ khả năng đó theo nhiều cách khác nhau nên việc tối ưu hóa trở nên khó hơn
      Nghĩ lại thì đây có vẻ là một điều khá hiển nhiên
  • Khi chuyển từ thay đổi #5 sang #6, việc inline caches và mô hình đối tượng hidden-class tạo ra phần lớn mức tăng hiệu năng khiến tôi cảm thấy rất giống với cách V8 hay JSC đã trở nên nhanh hơn trong lịch sử
    Điểm mà một interpreter ngây thơ chết hẳn cuối cùng vẫn là dynamic dispatch của truy cập thuộc tính, còn phần còn lại cho cảm giác tương đối gần với rounding error
    Tôi cũng thích việc bài viết trình bày để có thể nhìn riêng mức đóng góp của từng bước. Các bài về hiệu năng thường chỉ ném ra con số cuối cùng rồi thôi

    • Chi tiết triển khai đặc biệt thú vị ở #6 là làm sao thực hiện inline caching trong một interpreter đi trực tiếp trên AST
      Ở interpreter dạng bytecode, chỉ cần vá vào offset ổn định trong luồng bytecode nên vị trí ghi lại IC là điều tự nhiên
      Nhưng ở đây vị trí cache lại là nút AST, nên việc @pizlonator dùng constructCache<> để dựng specialized AST node đè in-place lên generic node là điều rất ấn tượng
      Cuối cùng nó trông như self-modifying code ở cấp AST
      Tuy vậy, cách này đòi hỏi mutable AST nodes, nên xung đột với giả định AST bất biến mà nhiều compiler trông đợi, như chia sẻ subtree hay biên dịch song song
      Với một interpreter đơn luồng thì khá gọn gàng, nhưng nếu interpreter thay đổi node trong khi cùng AST đó đang được JIT compile trên một luồng nền thì có vẻ sẽ thành vấn đề
    • Tôi đồng ý với định hướng chung, nhưng cũng nên có một lưu ý nhỏ rằng đây rốt cuộc chỉ là kết quả trên một benchmark cụ thể
      Theo tôi nó có thể không đại diện thật tốt cho phần lớn mã thực tế trong môi trường làm việc
      Tôi nghĩ vậy vì đoạn nói rằng tối ưu hóa sqrt mang lại cải thiện 1.6%
      Muốn có mức cải thiện như vậy thì ngay từ đầu benchmark hẳn phải dành hơn 1.6% thời gian cho việc đó, điều này khá gây ngạc nhiên
      Xem repo git thì có vẻ điều đó thực sự đã xảy ra trong mô phỏng nbody
  • Tôi thấy còn thú vị hơn vì gần đây tôi cũng vừa công bố phiên bản đầu tiên của AST-walking interpreter của mình
    Mục tiêu của tôi là hiểu ở mức nền tảng xem cần những gì để tạo ra một ngôn ngữ thông dịch
    Tôi không muốn đưa sự phức tạp của tối ưu hóa vào, mà chỉ tập trung làm sao để chính mình có thể hiểu được code Rust của mình
    Nhưng tôi đã ngạc nhiên vì chỉ riêng việc dùng Rust, ngôn ngữ tôi yêu thích, cũng đã cho hiệu năng khá tốt
    Hơn nữa việc Rust lo ownership và lifetimes giúp tôi không cần garbage collector riêng, điều đó cũng giống như một phần thưởng thêm
    Tất nhiên hiện giờ tôi vẫn đang khá dè dặt dựa vào clone để tránh địa ngục lifetime ở những phần như closure, nhưng dù vậy tốc độ và profile bộ nhớ vẫn đủ ổn
    Nếu bạn quan tâm đến một tree-walking interpreter đơn giản, dễ hiểu viết bằng Rust thì có thể xem trình thông dịch của tôi là gluonscript

  • Bài viết thật sự rất hay
    Đặc biệt, arc về Arguments, tức là dòng chuyển từ #7 sang #13, chạm vào trải nghiệm của tôi rất giống
    Trước đây khi làm một async step evaluator bằng Rust, tôi từng tin rằng borrow sẽ mang lại lợi ích nên đã đi khá sâu với Cow<'_, Input>
    Trong microbenchmark thì trông ổn, nhưng ở workload thực tế, discriminant của Cow và độ phức tạp liên quan đến lifetime lan sang mọi combinator sau await đầu tiên, khiến việc inline bị sụp đổ đáng kể và lý do dùng Cow cũng biến mất
    Cuối cùng tôi chuyển sang NoInput / OneInput / MultiInput(Vec) ở ranh giới evaluator, nhìn thì thô nhưng rốt cuộc đi đến gần như đúng điểm tương tự với việc tách ZeroArguments / OneArgument / TwoArguments ở đây
    Một điều tôi vẫn luôn tò mò là liệu tác giả có thử chồng thêm type specialization lên trên arity specialization ở nhánh native hay không
    Ví dụ, nếu đi theo kiểu binary thì có vẻ còn có thể loại bỏ luôn cả kiểm tra isInt
    Tôi đoán hoặc là bài toán kích thước code không cân, hoặc là phía object đã có IC ăn gần hết các hot path nên native fast path không ảnh hưởng nhiều
    Tôi khá tò mò là trường hợp nào

  • Đây thực sự là công việc thú vị và được làm rất tốt
    Tôi cũng từng làm việc tương tự, nhưng với Scheme là một ngôn ngữ thiên về hàm hơn
    Ở đây tối ưu hóa đối tượng mang lại lợi ích lớn nhất, còn với tôi thì closures mới là chiến trường quan trọng nhất
    Điều thú vị là cách tối ưu hóa tự thân lại khá giống nhau
    Theo tôi, gần như toàn bộ câu trả lời để làm cho Scheme đủ nhanh đều nằm trong Three implementation models for scheme
    Chỉ khác là bên đó có đi qua một mức biên dịch nào đó, nên không phải mô hình thông dịch trực tiếp nguyên AST như ở đây

  • Khá thú vị, cảm ơn vì đã chia sẻ
    Tôi cũng thấy mình muốn một ngày nào đó đào sâu chủ đề này
    Và việc repo trên Github là 99.7% HTML và 0.3% C++ cũng vừa buồn cười vừa ấn tượng
    Nó giống như bằng chứng cho thấy interpreter thực sự rất nhỏ

    • Trông vậy là vì đã commit cả site được tạo tĩnh vào
      Phần site bị phình ra không cần thiết vì cách tạo mã cho trình duyệt
      Dù vậy thì bản thân interpreter đúng là rất nhỏ
  • Tôi tò mò không biết trong quá trình làm việc này có học được điều gì có thể giúp cải thiện chính fil c hay không

    • Chắc chắn tôi cảm thấy cách xử lý unions cần có một lời giải tốt hơn
      Và tôi cũng học được rằng chi phí xử lý phương thức của value object bằng outline call là khá lớn
  • Tôi thấy có Lua trong đó, nhưng cũng ước là có cả LuaJIT nữa

    • Tôi đoán LuaJIT sẽ đánh bại Zef hoàn toàn
      Không, xét đến mức độ engineering đã được đổ vào đó thì thực ra tôi còn kỳ vọng là nó phải như vậy
      Có rất nhiều runtime có thể đưa vào nhưng tôi không đưa hết tất cả
      Và việc PUC Lua nhanh hơn QuickJS hay Python khá nhiều cũng rất ấn tượng
  • Tôi tò mò trải nghiệm dùng Fil-C thực tế thế nào, liệu nó có tính hữu dụng thực tiễn trong công việc thật không

    • Tôi là chính Fil nên xin nói trước là có thiên kiến
      Nhưng dù vậy, trong dự án này nó đã giúp ích khá thực tế
      Nó bắt được nhiều vấn đề an toàn bộ nhớ một cách deterministic, khiến việc thiết kế mô hình đối tượng dễ hơn rất nhiều so với khi không có nó
      Ngoài ra, C++ có GC chính xác mang lại cảm giác như một mô hình lập trình thực sự rất tốt
      So với C++ thông thường tôi có cảm giác năng suất tăng khoảng 1.5 lần, và ngay cả khi so với các ngôn ngữ GC khác thì cũng có vẻ phát triển nhanh hơn khoảng 1.2 lần
      Theo tôi là nhờ hệ sinh thái API của C++ rất phong phú, còn lambdas, templates và hệ thống class thì đã cực kỳ trưởng thành
      Tất nhiên tôi cũng thừa nhận là mình có thiên vị trên nhiều phương diện
      Tôi đã tự làm Fil-C++ và cũng đã dùng C++ khoảng 35 năm rồi
  • Tôi tò mò trình biên dịch YOLO-C/C++ được nhắc đến trong bài là gì
    Tìm kiếm cũng không ra mấy và chatgpt có vẻ cũng không biết

    • Tác giả của Fil-C và cũng là tác giả của ngôn ngữ này dùng cách nói Yolo-C/C++ để chỉ C/C++ thông thường không có Fil-C