Cách tạo ra một trình thông dịch ngôn ngữ động nhanh
(zef-lang.dev)- 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, watchpoint và việ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++ và 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*
- Các giá trị có thể chứa là double, số nguyên 32 bit,
- 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
0xffff000000000000cho số nguyên
- Dựa trên giả định rằng giá trị con trỏ không nhỏ hơ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
- Sử dụng tagged value 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ục và tham 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
- 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ã
-
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::evaluatebị override ở nhiều nơi
- Cấu trúc với các phương thức virtual
- Lạm dụng chuỗi
- Nút AST
Getlưustd::stringmô tả tên biến - Mỗi lần truy cập biến đều dùng chuỗi đó
- Nút AST
- Lạm dụng hash table
- Khi thực thi
Get, hệ thống tra cứustd::unordered_mapbằng khóa chuỗi
- Khi thực thi
- 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
- Dùng Fil-C++
-
Đặ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
DotCallmang tên toán tử nữa, mà tạo nút AST riêng cho từng toán tử - Trong Zef,
a + bvàa.add(b)là như nhau- Trước đây,
a + bđược phân tích thànhDotCall(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ử
DotCalltruyền chuỗi vàoValue::callMethodValue::callMethodthực hiện nhiều lần so sánh chuỗi
- Trước đây,
- 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::evaluateoverride khác nhau cho từng toán tử - Mỗi nút gọi trực tiếp đường đi nhanh
Valuecủa toán tử tương ứng - Ví dụ,
a + bgọiBinary<lambda for add>::evaluaterồi gọiValue::add
- Tận dụng template và lambda để cung cấp các
- 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
- Trình phân tích cú pháp không còn tạo toán tử dưới dạng nút
-
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 += bvẫ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]
- Get tương ứng với đọc biến
- Mỗi lời gọi ảo sử dụng macro
SPECIALIZE_NEW_RMW- SetRMW là
id += value - DotSetRMW là
expr.id += value - SubscriptRMW là
expr[index] += value
- SetRMW là
- 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,subscriptvà 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ế
- Được chọn vì phải xử lý cả ba đường đi
- 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
- Các toán tử thông thường đã nhanh hơn, nhưng dạng RMW như
-
Tối ưu hóa #3: Tránh kiểm tra IntObject
- Điểm nghẽn là đường đi nhanh của
ValuedùngisInt(), cònisIntSlow()bên trong thực hiện lời gọi ảoObject::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
- 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à
- Sau tối ưu hóa, đường đi nhanh của
Valuechỉ 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
- Logic xử lý IntObject được chuyển vào chính
- 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
- Điểm nghẽn là đường đi nhanh của
-
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ómObject::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
Symbolhash-consed - Thêm lớp
Symbolmớ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*
- Được triển khai trong
- Dùng symbol được chuẩn bị sẵn thay cho literal chuỗi
- Ví dụ, dùng
Symbol::subscriptthay cho"subscript"
- Ví dụ, dùng
- Nhiều chữ ký hàm đã được đổi từ
const std::string&sangSymbol* - 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
- Trình thông dịch ban đầu dùng
-
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.hlà vì nó dùng các header phải includevalue.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
- Tái cấu trúc quy mô lớn cách hoạt động của
-
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
Contextgiữ một bảng băm chứa các biến của scope đó
- Mỗi
- Đố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
- Mỗi đối tượng giữ một bảng băm ánh xạ các lớp mà nó là instance tới
- Lý do cần cấu trúc này là vì kế thừa và nested scope
- Khi
Barkế thừaFoo,BarvàFooclose 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
- Khi
- 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
Contextquyết định
Contextvẫ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 passresolvecủ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
- Trước đây, mỗi lexical scope đều cấp phát một đối tượng
-
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ùng mànameđượ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
- Là kỹ thuật ghi nhớ kiểu động của
-
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
CacheRecipekhắpContext,ClassObject,Package- Thu thập thông tin về quá trình truy cập
- Các hàm đánh giá AST như
Dot::evaluatetruyềnCacheRecipethu được từ phép toán đa hình mà chúng thực hiện, cùng vớithis, vàoconstructCache<> 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 step và watchpoint
- Biên dịch chuyên biệt AST node mới dựa trên
- 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
cachedoconstructCache<>quyết định
- Trước tiên thử gọi nhanh qua đối tượng
-
Watchpoint
- Đưa ra ví dụ lexical scope có biến
x, bên trong có lớpFoo, và phương thức củaFootruy cậpx - Nếu bên trong
Fookhông có hàm hay biến tênx, có vẻ như nó có thể đọc trực tiếpxở 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à
xbên ngoài - Để xử lý khả năng thay đổi này, inline cache thiết lập
Watchpointtại runtime - Trong ví dụ, watchpoint được dùng để theo dõi liệu tên này có bị override hay không
- Đưa ra ví dụ lexical scope có biến
-
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ế Storage và offset đã 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::tryCallMethodhiện thực toàn bộ phương thức bằng cách chặn lời gọi ảoObject::tryCallMethod - Trong mô hình đối tượng mới,
Objectkhông có vtable và cũng không có phương thức ảo - Thay vào đó,
Object::tryCallMethodủy quyền sangobject->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 đó
- Trước đây,
- 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
- Ban đầu bắt đầu bằng việc viết một phiên bản
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
optionallà vì trong một số trường hợp biên, cần phân biệt hai dạng sauo.gettero.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.NestedClasso.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ố và 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 đó
- Bên gọi phải cấp phát
- 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ỏ
malloccủa vùng lưu trữ nềnvector - Trong Fil-C++ thì bản thân
std::optionalcũng cấp phát trên heap- Ngay cả khi không có
std::optional, việc truyềnconst 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
- Ngay cả khi không có
- 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
- Trước thay đổi, trình thông dịch Zef truyền đối số hàm dưới dạng
-
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ộ
fchỉ hiển thị với instance
- Lưu giá trị nhận từ constructor vào biến cục bộ
- Ngay cả các instance cùng kiểu cũng không thể truy cập
fcủa đối tượng khác- Ví dụ
fn nope(o) o.f println(Foo(42).nope(Foo(666)))o.fbên trongnopekhông thể truy cậpfcủao
- Ví dụ
- 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.fkhông phải là đọc trường mà là yêu cầu gọi phương thức tênf
- Vì thế mẫu sau xuất hiện thường xuyên
my ffn f f- Tức là một phương thức tên
ftrả 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 fvàfn f f
- Dạng rút gọn của
- 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
- Trọng tâm là
- Quy tắc suy luận
Block::inferGettersẽ suy luận mình là getter nếu mọi thứ nó chứa đều có thể suy luận thành getterGet::inferGettersuy luận chính nó là getter và trả về offset cần nạpContext::tryGetFieldOffsetschỉ trả vềOffsetskhông rỗng khi trường đó chắc chắn tồn tại trong lexical scope nơi getter sẽ chạyUserFunctionsẽ resolve thành một lớp conFunctionchuyê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àoClassObject, đồ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
- Trong suy luận setter, cần pattern matching mẫu
-
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::tryCallMethodvàClassObject::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
- Khi xảy ra inline cache miss trong lời gọi phương thức, phải đi xuống
-
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::optionalcầ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
FilPizlonatorthự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::optionalthườ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
- Trong Fil-C++, do bệnh lý trình biên dịch liên quan đến union,
-
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
- Khi callee không cần, caller có thể tránh cấp phát đối tượng
- 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
ZeroArgumentsmang nghĩa là gọi hàm không có đối số
- Trước đây,
- 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::getArglà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
- Thực hiện khởi tạo tường minh cho từng trường hợp
- 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
- 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
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
Valuelà hàm thành viên củaValuevà cần đối sốconst Value*ngầm định - Với cấu trúc này, caller phải cấp phát
Valuetrê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
Valuetrên heap
- Vì vậy, mã gọi slow path sẽ cấp phát
- Sau thay đổi, các phương thức này được chuyển thành
staticvà truyềnValuetheo 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ảiinthoặcdoublehay 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
- Không áp dụng cho dạng như
- Thay đổi lần này cho phép
Dotđược chuyên biệt hóa chovalue.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
toStringgầ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
intthà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
- Áp dụng chuyên biệt hóa
-
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,3mỗi lần - Thay đổi lần này chuyên biệt hóa node
ArrayLiteralcho 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
- Mã như
-
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
Valuetheo tham chiếu, lần này cho slow path củacallOperator - 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
- Á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
-
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
ZASSERTdà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
ASSERTnội bộ- Chỉ thực hiện assert khi
ASSERTS_ENABLEDđược thiết lập
- Chỉ thực hiện assert khi
- 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 không sound là vì lời gọi GC Fil-C++ hiện có bị thay thành lời gọi
- Lý do suboptimal là vì bộ cấp phát GC thực tế nhanh hơn
calloccủ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,geomeancho từng trình thông dịch -
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 Thay đổi #1: Toán tử trực tiếp
nbody2.1891splay12.0233richards1.6935deltablue5.2331geomean3.9076
-
Zef Thay đổi #2: RMW trực tiếp
nbody2.0130splay11.9987richards1.6367deltablue5.0994geomean3.7677
-
Zef Thay đổi #3: Tránh IntObject
nbody1.9922splay11.8824richards1.6220deltablue5.0646geomean3.7339
-
Zef Thay đổi #4: Symbol
nbody1.5782splay9.9577richards1.4116deltablue4.4593geomean3.1533
-
Zef Thay đổi #5: Inline giá trị
nbody1.4982splay9.7723richards1.3890deltablue4.3536geomean3.0671
-
Zef Thay đổi #6: Mô hình đối tượng và inline cache
nbody0.3884splay3.3609richards0.2321deltablue0.6805geomean0.6736
-
Zef Thay đổi #7: Đối số
nbody0.3160splay2.6890richards0.1653deltablue0.4738geomean0.5077
-
Zef Thay đổi #8: Getter
nbody0.2988splay2.6919richards0.1564deltablue0.4260geomean0.4809
-
Zef Thay đổi #9: Setter
nbody0.2850splay2.6690richards0.1514deltablue0.4072geomean0.4651
-
Zef Thay đổi #10:
callMethodinlinenbody0.2533splay2.6711richards0.1513deltablue0.4032geomean0.4506
-
Zef Thay đổi #11: Hashtable
nbody0.1796splay2.6528richards0.1379deltablue0.3551geomean0.3906
-
Zef Thay đổi #12: Tránh
std::optionalnbody0.1689splay2.6563richards0.1379deltablue0.3518geomean0.3839
-
Zef Thay đổi #13: Đối số chuyên biệt hóa
nbody0.1610splay2.5823richards0.1350deltablue0.3372geomean0.3707
-
Zef Thay đổi #14: Cải thiện các slow path của Value
nbody0.1348splay2.5062richards0.1241deltablue0.3076geomean0.3367
-
Zef Thay đổi #15: Hợp nhất trùng lặp
DotSetRMW::evaluatenbody0.1342splay2.5047richards0.1256deltablue0.3079geomean0.3375
-
Zef Thay đổi #16:
sqrtnhanhnbody0.1274splay2.5045richards0.1251deltablue0.3060geomean0.3322
-
Zef Thay đổi #17:
toStringnhanhnbody0.1282splay2.2664richards0.1275deltablue0.2964geomean0.3235
-
Zef Thay đổi #18: Chuyên biệt hóa literal mảng
nbody0.1295splay1.6661richards0.1250deltablue0.2979geomean0.2992
-
Zef Thay đổi #19: Tối ưu hóa
callOperatorcủa Valuenbody0.1208splay1.6698richards0.1143deltablue0.2713geomean0.2810
-
Zef Thay đổi #20: Cấu hình C++ tốt hơn
nbody0.1186splay1.6521richards0.1127deltablue0.2635geomean0.2760
-
Zef Thay đổi #21: Không dùng assert
nbody0.1194splay1.6504richards0.1127deltablue0.2619geomean0.2759
-
Zef trong Yolo-C++
nbody0.0233splay0.3992richards0.0309deltablue0.0784geomean0.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ứ?
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ấ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
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
Ở 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ượngCuố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 đề
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ấtCuố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 ở đâyMộ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
isIntTô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ỏ
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
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
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
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