Lập trình

Các trình thông dịch Javascript và V8 engine: tìm hiểu và ứng dụng vào việc tối ưu mã nguồn.

Huy Erick

#BKFA_Team: Không có gì là không thể! Chúng ta đều biết rằng máy tính chỉ hiểu được mã máy (machine code), và các ngôn ngữ lập trình cao cấp khác đều cần một bộ biên...

#BKFA_Team: Không có gì là không thể! Chúng ta đều biết rằng máy tính chỉ hiểu được mã máy (machine code), và các ngôn ngữ lập trình cao cấp khác đều cần một bộ biên dịch hoặc thông dịch mã nguồn sang mã máy. Ví dụ, C có GCC (GNU complier collection), Java có Javac (Java compiler), Ruby có JRuby (trình thông dịch xây dựng trên Java)... Javascript cũng có các trình thông dịch riêng của mình, và chúng ta gọi chúng là Javascript engine.

Hôm nay, chúng ta sẽ tìm hiểu sâu hơn về đại diện quan trọng nhất trong số đó - V8 engine, một trình thông dịch tuyệt vời được phát triển bởi các kỹ sư tài ba tại Google.

Tìm hiểu về V8 Engine

V8 engine là một chương trình có khả năng thông dịch mã nguồn Javascript thành mã máy mà máy tính có thể hiểu được. Nó hoạt động như một trình thông dịch thông thường hoặc một trình biên dịch just-in-time (JIT compiler). Các trình thông dịch thường được tích hợp trong các trình duyệt hoặc các công nghệ xây dựng trên nền Javascript như NodeJS, và chúng có vai trò như trái tim của các chương trình.

Dưới đây là một số Javascript engine phổ biến khác trên thế giới:

  • V8: Được sử dụng trong hầu hết các trình duyệt nhân Chromium, bao gồm trình duyệt Cốc Cốc của Việt Nam.
  • Rhino: Được quản lý bởi Mozilla Foundation và viết bằng Java.
  • SpiderMonkey: Là trình thông dịch Javascript lâu đời nhất, được sử dụng trong trình duyệt Firefox.
  • JavaScriptCore: Được phát triển bởi Apple cho trình duyệt Safari.
  • Chakra (JScript9): Được phát triển bởi Microsoft và dành cho trình duyệt Internet Explorer.
  • Chakra (JavaScript): Được sử dụng trong trình duyệt Microsoft Edge.
  • JerryScript: Được thiết kế tối ưu cho các nền tảng Internet of Things.

Trong khi các ông lớn khác đưa ra các trình duyệt riêng của mình, Google cũng không ngoại lệ và đã phát triển V8 engine để chạy trên hầu hết các trình duyệt nhân Chromium. V8 là một engine trẻ tuổi và đang được phát triển trên repository GitHub: https://github.com/v8. Nó được viết bằng c+ + và được sử dụng rộng rãi trong lõi của NodeJS.

Đặc điểm của V8 Engine

V8 engine được thiết kế để đạt được tốc độ xử lý nhanh nhất, như tên gọi của nó - một engine (động cơ). Nó cũng sử dụng JIT compiler để biên dịch mã như các engine phổ biến khác như Rhino và SpiderMonkey. Điểm khác biệt lớn nhất của V8 là nó không thông dịch qua bytecode hoặc bất kỳ mã trung gian nào khác.

V8 engine bao gồm hai bộ biên dịch chính:

  • Full codegen: Đây là một bộ biên dịch đơn giản và nhẹ nhàng, nhưng mã máy biên dịch không được tối ưu.
  • Crankshaft: Đây là một bộ biên dịch phức tạp và chậm hơn (JIT), nhưng mã máy biên dịch được tối ưu tốt hơn.

V8 engine sử dụng nhiều luồng để thực thi biên dịch, bao gồm luồng chính để đọc và biên dịch mã nguồn, và các luồng nhỏ để tối ưu mã, quyết định việc tối ưu và gửi cho Crankshaft, và dọn dẹp bộ nhớ thông qua garbage collector.

Khi mã nguồn Javascript được biên dịch lần đầu tiên, V8 sẽ đẩy mã code này vào bộ biên dịch full codegen. Mã máy sẽ được biên dịch ngay lập tức do đây là trình biên dịch nhanh và nhẹ. Tuy nhiên, trong lần thứ hai, chẳng hạn khi bạn làm mới trang web, nếu luồng profiler đã thu thập đủ dữ liệu để quyết định mã nào cần tối ưu hóa, V8 sẽ ưu tiên đẩy chúng vào bộ biên dịch Crankshaft. Crankshaft sẽ biên dịch mã thành một cây high-level static single-assignment (SSA), là biểu diễn đồ thị Hydrogen. Phần lớn công việc tối ưu hóa được thực hiện ở giai đoạn này.

Các đặc điểm quan trọng khác của V8 engine bao gồm:

  • Inlining: Đây là quá trình đầu tiên V8 thực hiện để tối ưu quá trình biên dịch. Nôm na, inlining là quá trình thay thế dòng gọi hàm bằng nội dung của hàm đó.
  • Hidden class: Javascript là ngôn ngữ lập trình động, điều đó có nghĩa là thuộc tính của một đối tượng có thể được thêm hoặc xóa dễ dàng. Đa số các trình thông dịch Javascript sử dụng cấu trúc băm (hash function) để lưu trữ giá trị các thuộc tính trong bộ nhớ. Điều này làm cho quá trình truy xuất giá trị thuộc tính trong Javascript tốn kém hơn so với các ngôn ngữ khác như Java hoặc C, nơi các thuộc tính của đối tượng là cố định. V8 engine sử dụng một cách tiếp cận hoàn toàn khác để giải quyết vấn đề này, đó là hidden class. Hidden class hoạt động tương tự như cách fixed object layouts hoạt động trong ngôn ngữ Java, trừ việc hidden class được khởi tạo trong quá trình runtime.
  • Inline Caching: Đây là quá trình mà V8 theo dõi và "đoán" sự lặp lại của cùng một phương thức trên cùng một loại đối tượng. V8 sử dụng một bộ cache để lưu trữ loại đối tượng sẽ được truyền vào hàm, từ đó tăng tốc việc sử dụng đối tượng này thông qua việc sử dụng chung hidden class.
  • Garbage Collector: V8 sử dụng cách tiếp cận mark-and-sweep (đánh dấu và quét) cho garbage collector. Trong quá trình đánh dấu, các đối tượng không còn được sử dụng trong tương lai được đánh dấu thông qua quá trình duyệt tìm trên heap. Sau đó, quá trình quét giải phóng các vùng nhớ này.
  • Ignition và TurboFan: V8 engine phiên bản 5.9 ra đời vào đầu năm 2017, giới thiệu một kỹ thuật xử lý đường ống mới. Kỹ thuật này mang lại hiệu suất và tiết kiệm bộ nhớ tốt hơn đáng kể so với các phiên bản trước. Nó được xây dựng trên Ignition, trình thông dịch của V8, và TurboFan, trình biên dịch tối ưu hóa mới nhất của V8.

Kết luận

Qua việc tìm hiểu các thành phần cơ bản của V8 engine, chúng ta có thể tận dụng những đặc điểm này để tối ưu việc biên dịch mã nguồn Javascript. Nhớ những điểm sau để tận dụng hiệu quả V8 engine:

  1. Thứ tự khởi tạo thuộc tính của đối tượng: Khởi tạo thuộc tính theo cùng một thứ tự cho các đối tượng cùng lớp để tận dụng việc chia sẻ hidden class.
  2. Thuộc tính động: Hãy khởi tạo tất cả thuộc tính cần thiết trong hàm khởi tạo (constructor) thay vì thêm thuộc tính sau đó, vì điều này tốn kém và làm thay đổi hidden class.
  3. Phương thức: Các phương thức đã được gọi nhiều lần sẽ thực thi nhanh hơn so với các phương thức mới được gọi lần đầu tiên.
  4. Mảng: Tránh khởi tạo mảng với key không liên tiếp (gây ra hidden class riêng), thay vào đó hãy đối chỗ hoặc dịch mảng để tạo ra một dải các key liên tiếp chứa giá trị trong mảng.

Tài liệu tham khảo: BKFA Team

1