Có hai loại ngôn ngữ lập trình: ngôn ngữ lập trình thu gom rác và ngôn ngữ lập trình yêu cầu quản lý bộ nhớ theo cách thủ công. Ví dụ về ngôn ngữ lập trình hướng đối tượng có Kotlin, PHP hoặc Java. Ví dụ về ngôn ngữ lập trình này là C, C++ hoặc Rust. Theo nguyên tắc chung, các ngôn ngữ lập trình cấp cao hơn có nhiều khả năng có tính năng thu gom rác làm tính năng tiêu chuẩn. Trong bài đăng trên blog này, chúng tôi tập trung vào các ngôn ngữ lập trình thu gom rác và cách biên dịch các ngôn ngữ đó sang WebAssembly (Wasm). Nhưng trước tiên, thu gom rác (thường được gọi là GC) là gì?
Hỗ trợ trình duyệt
Thu gom rác
Nói một cách đơn giản, ý tưởng của việc thu gom rác là cố gắng lấy lại bộ nhớ do chương trình phân bổ nhưng không còn được tham chiếu nữa. Bộ nhớ như vậy được gọi là rác. Có nhiều chiến lược để triển khai tính năng thu gom rác. Một trong số đó là đếm tham chiếu, trong đó mục tiêu là đếm số lượng tham chiếu đến các đối tượng trong bộ nhớ. Khi không còn tham chiếu nào đến một đối tượng, đối tượng đó có thể được đánh dấu là không còn được sử dụng và sẵn sàng để thu thập rác. Trình thu gom rác của PHP sử dụng tính năng đếm tham chiếu và việc sử dụng hàm xdebug_debug_zval()
của tiện ích Xdebug cho phép bạn xem xét kỹ hơn. Hãy xem xét chương trình PHP sau.
<?php
$a= (string) rand();
$c = $b = $a;
$b = 42;
unset($c);
$a = null;
?>
Chương trình gán một số ngẫu nhiên được truyền vào một chuỗi cho một biến mới có tên là a
. Sau đó, hàm này sẽ tạo hai biến mới là b
và c
, đồng thời chỉ định giá trị a
cho các biến đó. Sau đó, hàm này gán lại b
cho số 42
, rồi huỷ thiết lập c
. Cuối cùng, hàm này đặt giá trị của a
thành null
. Chú thích từng bước của chương trình bằng xdebug_debug_zval()
, bạn có thể thấy bộ đếm tham chiếu của trình thu gom rác đang hoạt động.
<?php
$a= (string) rand();
$c = $b = $a;
xdebug_debug_zval('a');
$b = 42;
xdebug_debug_zval('a');
unset($c);
xdebug_debug_zval('a');
$a = null;
xdebug_debug_zval('a');
?>
Ví dụ trên sẽ xuất ra các nhật ký sau đây, trong đó bạn thấy số lượng tham chiếu đến giá trị của biến a
giảm dần sau mỗi bước, điều này hợp lý với trình tự mã. (Tất nhiên, số ngẫu nhiên của bạn sẽ khác.)
a:
(refcount=3, is_ref=0)string '419796578' (length=9)
a:
(refcount=2, is_ref=0)string '419796578' (length=9)
a:
(refcount=1, is_ref=0)string '419796578' (length=9)
a:
(refcount=0, is_ref=0)null
Có những thách thức khác với việc thu gom rác, chẳng hạn như phát hiện chu kỳ, nhưng đối với bài viết này, bạn chỉ cần hiểu cơ bản về tính năng đếm tham chiếu là đủ.
Ngôn ngữ lập trình được triển khai trong các ngôn ngữ lập trình khác
Có thể bạn sẽ thấy khó hiểu, nhưng các ngôn ngữ lập trình được triển khai trong các ngôn ngữ lập trình khác. Ví dụ: môi trường thời gian chạy PHP chủ yếu được triển khai trong C. Bạn có thể xem mã nguồn PHP trên GitHub. Mã thu gom rác của PHP chủ yếu nằm trong tệp zend_gc.c
. Hầu hết các nhà phát triển sẽ cài đặt PHP thông qua trình quản lý gói của hệ điều hành. Tuy nhiên, nhà phát triển cũng có thể tạo PHP từ mã nguồn. Ví dụ: trong môi trường Linux, các bước ./buildconf && ./configure && make
sẽ tạo PHP cho môi trường thời gian chạy Linux. Tuy nhiên, điều này cũng có nghĩa là bạn có thể biên dịch môi trường thời gian chạy PHP cho các môi trường thời gian chạy khác, chẳng hạn như Wasm.
Các phương thức truyền thống để chuyển đổi ngôn ngữ sang môi trường thời gian chạy Wasm
Độc lập với nền tảng mà PHP đang chạy, các tập lệnh PHP được biên dịch thành cùng một mã byte và chạy bằng Công cụ Zend. Zend Engine là một trình biên dịch và môi trường thời gian chạy cho ngôn ngữ tập lệnh PHP. Mô-đun này bao gồm Máy ảo Zend (VM), bao gồm Trình biên dịch Zend và Trình thực thi Zend. Các ngôn ngữ như PHP được triển khai trong các ngôn ngữ cấp cao khác như C thường có các tính năng tối ưu hoá nhắm đến các cấu trúc cụ thể, chẳng hạn như Intel hoặc ARM, đồng thời yêu cầu một phần phụ trợ khác nhau cho mỗi cấu trúc. Trong ngữ cảnh này, Wasm đại diện cho một cấu trúc mới. Nếu máy ảo có mã dành riêng cho cấu trúc, chẳng hạn như biên dịch đúng lúc (JIT) hoặc biên dịch trước (AOT), thì nhà phát triển cũng triển khai phần phụ trợ cho JIT/AOT cho cấu trúc mới. Phương pháp này rất hợp lý vì thường thì bạn chỉ có thể biên dịch lại phần chính của cơ sở mã cho mỗi cấu trúc mới.
Do Wasm ở cấp thấp, nên bạn có thể thử áp dụng phương pháp tương tự: Biên dịch lại mã máy ảo chính bằng trình phân tích cú pháp, hỗ trợ thư viện, thu gom rác và trình tối ưu hoá sang Wasm, đồng thời triển khai phần phụ trợ JIT hoặc AOT cho Wasm nếu cần. Điều này đã có thể thực hiện được kể từ MVP Wasm và hoạt động hiệu quả trong thực tế trong nhiều trường hợp. Trên thực tế, PHP được biên dịch sang Wasm là yếu tố hỗ trợ WordPress Playground. Tìm hiểu thêm về dự án này trong bài viết Tạo trải nghiệm WordPress trong trình duyệt bằng WordPress Playground và WebAssembly.
Tuy nhiên, PHP Wasm chạy trong trình duyệt trong ngữ cảnh của ngôn ngữ lưu trữ JavaScript. Trong Chrome, JavaScript và Wasm chạy trong V8, công cụ JavaScript nguồn mở của Google triển khai ECMAScript như được chỉ định trong ECMA-262. Ngoài ra, V8 đã có trình thu gom rác. Điều này có nghĩa là các nhà phát triển sử dụng, ví dụ: PHP được biên dịch sang Wasm, cuối cùng sẽ gửi một bản triển khai trình thu gom rác của ngôn ngữ được chuyển (PHP) đến trình duyệt đã có trình thu gom rác, điều này thật lãng phí. Đây là lúc WasmGC phát huy tác dụng.
Một vấn đề khác của phương pháp cũ là cho phép các mô-đun Wasm tạo GC của riêng chúng trên bộ nhớ tuyến tính của Wasm. Sau đó, không có sự tương tác nào giữa trình thu gom rác của riêng Wasm và trình thu gom rác được tạo trên ngôn ngữ được biên dịch sang Wasm. Điều này thường gây ra các vấn đề như rò rỉ bộ nhớ và các lần thu thập không hiệu quả. Việc cho phép các mô-đun Wasm sử dụng lại GC tích hợp hiện có sẽ giúp tránh được những vấn đề này.
Chuyển đổi ngôn ngữ lập trình sang môi trường thời gian chạy mới bằng WasmGC
WasmGC là một đề xuất của Nhóm cộng đồng WebAssembly. Việc triển khai MVP Wasm hiện tại chỉ có thể xử lý các số, tức là số nguyên và số thực, trong bộ nhớ tuyến tính và với đề xuất loại tham chiếu đang được vận chuyển, Wasm cũng có thể giữ lại các tham chiếu bên ngoài. WasmGC hiện thêm các loại vùng nhớ khối xếp cấu trúc và mảng, tức là hỗ trợ việc phân bổ bộ nhớ không tuyến tính. Mỗi đối tượng WasmGC có một loại và cấu trúc cố định, giúp các máy ảo dễ dàng tạo mã hiệu quả để truy cập vào các trường của chúng mà không có nguy cơ giảm tối ưu hoá mà các ngôn ngữ động như JavaScript có. Do đó, đề xuất này sẽ bổ sung tính năng hỗ trợ hiệu quả cho các ngôn ngữ được quản lý cấp cao vào WebAssembly, thông qua các loại vùng nhớ khối xếp cấu trúc và mảng cho phép trình biên dịch ngôn ngữ nhắm đến Wasm tích hợp với trình thu gom rác trong máy ảo lưu trữ. Nói một cách đơn giản, điều này có nghĩa là với WasmGC, việc chuyển đổi ngôn ngữ lập trình sang Wasm có nghĩa là trình thu gom rác của ngôn ngữ lập trình không còn cần phải là một phần của cổng nữa, mà thay vào đó, bạn có thể sử dụng trình thu gom rác hiện có.
Để xác minh tác động thực tế của điểm cải tiến này, nhóm Wasm của Chrome đã biên dịch các phiên bản điểm chuẩn Fannkuch (phân bổ cấu trúc dữ liệu khi hoạt động) từ C, Rust và Java. Tệp nhị phân C và Rust có thể có kích thước từ 6,1 K đến 9,6 K tuỳ thuộc vào các cờ trình biên dịch, trong khi phiên bản Java nhỏ hơn nhiều chỉ ở mức 2,3 K! C và Rust không bao gồm trình thu gom rác, nhưng vẫn đóng gói malloc/free
để quản lý bộ nhớ. Lý do Java nhỏ hơn ở đây là vì ngôn ngữ này không cần đóng gói bất kỳ mã quản lý bộ nhớ nào. Đây chỉ là một ví dụ cụ thể, nhưng nó cho thấy rằng các tệp nhị phân WasmGC có khả năng rất nhỏ, ngay cả trước khi có bất kỳ công việc đáng kể nào về việc tối ưu hoá kích thước.
Xem ngôn ngữ lập trình được chuyển đổi WasmGC đang hoạt động
Kotlin Wasm
Một trong những ngôn ngữ lập trình đầu tiên được chuyển sang Wasm nhờ WasmGC là Kotlin ở dạng Kotlin/Wasm. Bản minh hoạ, với mã nguồn do nhóm Kotlin cung cấp, được hiển thị trong danh sách sau.
import kotlinx.browser.document
import kotlinx.dom.appendText
import org.w3c.dom.HTMLDivElement
fun main() {
(document.getElementById("warning") as HTMLDivElement).style.display = "none"
document.body?.appendText("Hello, ${greet()}!")
}
fun greet() = "world"
Bây giờ, bạn có thể thắc mắc về điểm mấu chốt, vì mã Kotlin ở trên về cơ bản bao gồm các API OM JavaScript được chuyển đổi sang Kotlin. Điều này trở nên hợp lý hơn khi kết hợp với Compose Multiplatform, cho phép nhà phát triển xây dựng dựa trên giao diện người dùng mà họ có thể đã tạo cho các ứng dụng Kotlin trên Android. Hãy xem phần khám phá ban đầu về điều này bằng bản minh hoạ trình xem hình ảnh Kotlin/Wasm và khám phá mã nguồn của trình xem này, cũng do nhóm Kotlin cung cấp.
Dart và Flutter
Nhóm Dart và Flutter tại Google cũng đang chuẩn bị hỗ trợ cho WasmGC. Công việc biên dịch Dart sang Wasm gần như đã hoàn tất và nhóm đang nỗ lực hỗ trợ công cụ để phân phối các ứng dụng web Flutter được biên dịch sang WebAssembly. Bạn có thể đọc về trạng thái hiện tại của công việc trong tài liệu về Flutter. Bản minh hoạ sau đây là Bản xem trước Flutter WasmGC.
Tìm hiểu thêm về WasmGC
Bài đăng trên blog này chỉ mới đề cập đến một số khía cạnh và chủ yếu cung cấp thông tin tổng quan về WasmGC. Để tìm hiểu thêm về tính năng này, hãy xem các đường liên kết sau:
- Một cách mới để đưa các ngôn ngữ lập trình thu gom rác vào WebAssembly một cách hiệu quả
- Tổng quan về WasmGC
- MVP WasmGC
- WasmGC sau MVP
Lời cảm ơn
Bài viết này đã được Matthias Liedtke, Adam Klein, Joshua Bell, Alon Zakai, Jakob Kummerow, Clemens Backes, Emanuel Ziegler và Rachel Andrew xem xét.