11/08/2018, 20:34

Vài nét về V8 - Javascript Engine đằng sau Chrome và Node.js

V8 hay còn gọi là Chrome V8, là một Javascript engine được phát triển bởi Chromium Project, mục đích ban đầu là dành cho Google Chrome nói riêng và Chromium nói chung. Được ra mắt lần đầu vào tháng 12 năm 2008. 1 năm sau Nodejs và MongoDB ra mắt và cũng sử dụng V8 làm Javascript engine cho mình, ...

V8 hay còn gọi là Chrome V8, là một Javascript engine được phát triển bởi Chromium Project, mục đích ban đầu là dành cho Google Chrome nói riêng và Chromium nói chung. Được ra mắt lần đầu vào tháng 12 năm 2008. 1 năm sau Nodejs và MongoDB ra mắt và cũng sử dụng V8 làm Javascript engine cho mình, tuy nhiên với MongoDB thì đến phiên bản 3.2 đã chuyển từ V8 sang SpiderMonkey - Một Javascript engine khác (nhiều trang web vẫn còn nhầm lẫn điều này, cả Wikipedia tính tới thời điểm viết bài).

alt

Theo Wikipedia thì:

A JavaScript engine is a program or interpreter which executes JavaScript code.

Hiểu đơn giản thì nghĩa nó là: Javascript engine là một chương trình hoặc trình thông dịch thực thi mã Javascript.

Một Javascript engine có thể thông dịch như thường, hoặc biên dịch just-in-time từ Javascript thành bytecode (ta sẽ nói về cách này sau).

Dưới đây là một số các Javascript engine nổi tiếng:

  • SpiderMonkey  - Ông tổ của Javascript engine, được dùng trên trình duyệt web đầu tiên trên thế giới - Netscape Navigator, hiện tại đang được sử dụng trên Firefox, viết bằng C và C++.
  • Chakra - Là một Javascript engine cũng khá lâu đời, ban đầu được sử dụng trên Internet Explorer và biên dịch JScript, nay được dùng cho Microsoft Edge, viết bằng C++.
  • Rhino - Một Engine viết hoàn toàn bằng Java, cũng có lịch sử phát triển lâu đời từ Netscape Navigator, hiện tại được phát triển bởi Mozilla Foundation.
  • V8 - Như mình đã giới thiệu ở trên.

Nếu so sánh về về thời điểm ra mắt thì có lẽ V8 là một trong số các engine khá "trẻ". Đây là engine được thiết kế đặc biệt dành và tối ưu hóa hiệu năng cho các dự án Javascript lớn, như Node.js chẳng hạn. Còn cải thiện hiệu năng như thế nào thì còn phụ thuộc vào dòng bao nhiêu dòng code Javascript và bản chất của nó nữa. Ví dụ như nếu các chức năng trong ứng dụng của bạn có khuynh hướng lặp đi lặp lại, hiệu năng sẽ cao hơn các chức năng khác nhau mà chỉ chạy một lần. Để hiểu rõ điều này thì ta sẽ tìm hiểu về 3 tính năng chính của V8.

1. Fast Property Access

Hidden Class

Javascript là một ngôn ngữ động (dynamic programming language). Đa số các Javascript engine đều sử dụng cấu trúc dữ liệu dạng dictionary-like, mỗi lần truy cập các property thì phải cần một dynamic lookup (đây là một thuật ngữ khá khó dịch, các bạn có thể đọc giải thích ở #4 của câu hỏi này ) để solve vị trí của property này trong bộ nhớ. Cách này làm cho việc truy cập vào property của Javascript chậm hơn kha khá so với truy cập vào instance variable như Java: instance variable đặt ở các fixed offset, được xác định bởi compiler nhờ việc bố trí fixed object layout và định nghĩa bởi class chứa object đó, vì vậy để truy cập vào property thì thường chỉ cần 1 hoặc vài lệnh và cũng chỉ là vấn đề của memory load.

alt text

Để giải quyết điều này, V8 sẽ tự động tạo các hidden class (class ẩn), khi một property được thêm vào object, một hidden class sẽ được tạo ra (nhiều ông developer trong và ngoài nước vẫn thường nghĩ là tạo hash table).

Ví dụ:

function idol(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const name = new idol('Takizawa', 'Laura');

Khi idol() được gọi thì một hidden class sẽ được tạo, chẳng hạn ta gọi class là ABS130 chẳng hạn, thì field đầu tiên của Object idol trên là tham chiếu đến class ABS130.
alt text

Ban đầu, class ABS130 là một class rỗng, khi this.firstName = firstName được gọi, một hidden class nữa sẽ được tạo, ta gọi class này là ABS141 chẳng hạn, class ABS141 sẽ giữ một tham chiếu đến class ABS130 (tức là class trước đó), class ABS141 sẽ có một property là firstName , value của nó được lưu tại offset 0, còn pointer sẽ cập nhật đến class mới là class ABS141.

alt text

Tương tự với this.lastName = lastName, ta gọi class mới là MAS087:

alt text

Mặc dù nhìn có vẻ khá lằng nhằng, nhưng cách này khá hiệu quả vì khi tạo mới object idol trong các lần tiếp theo, nếu có cùng property và thứ tự giống nhau thì sẽ có cùng các hidden class vì tất cả các hidden class đều có thể tái sử dụng, còn nếu có khác nhau vài property thì V8 sẽ tạo thêm các branch (nhánh) riêng, ví dụ:

alt text

Inline cache

Đây là một kỹ thuật tối ưu hóa khá phổ biến và cũng khá lâu đời, phát triển lần đầu cho Smalltalk và khá hữu dụng cho mấy ngôn ngữ động. Inline cache (bộ nhớ đệm) sẽ quan sát các method được gọi được lặp lại có xu hướng xảy ra trên cùng 1 kiểu object. Khi method được gọi, nó sẽ cache lại kiểu của object vừa được truyền vào, và dùng nó làm giả thiết cho tham số truyền vào trong lần gọi tiếp theo. Nếu như giống nhau, nó sẽ trực tiếp trỏ đến đó và bỏ qua quá trình truy cập lằng nhằng để tìm đến property.
alt

2. Dynamic Machine Code Generation

Trong một project, khi code thì chắc chắn sẽ có kha khá các object có cùng hidden class, việc dùng hidden class để access vào property của object đó với sự hỗ trợ của inline cache và machine code sẽ cải thiện tốc độ khá lớn đối với các object cùng kiểu và cùng cách truy cập.

Khi access vào property của object, V8 sẽ cache lại hidden class đó. Trong quá trình này, V8 tối ưu hóa bằng cách lấy hidden class để dự đoán cho các lần access tiếp theo, và sẽ lấy thông tin của hidden class đó patch inline cache code để sử dụng hidden class. Nếu đoán đúng thì value của property sẽ được gán, còn sai thì sẽ remove đoạn đó đi.

Chẳng hạn ta đang access vào property firstName của object idol:

idol.firstName

Thì machine code sẽ được generate ra thế này:

# ebx = idol object
cmp [ebx,<hidden class offset>],<cached hidden class>
jne <inline cache miss>
mov eax,[ebx, <cached x offset>]

Nếu như hidden class chứa object idol không trùng với hidden class được cache từ trước, V8 runtime sẽ xử lí inline cache misses và patch lại inline cache code. Còn nếu trùng thì chỉ cần lấy property firstNameđã được cache ra là xong.

3. Garbage Collection

Garbage Collection thì có lẽ khá phổ biến và đa phần lập trình viên ai cũng biết, khi giải phóng bộ nhớ đã được cấp phát trong C/C++ thì ta phải tự làm bằng tay bằng delete và free, còn với mấy ngôn ngữ .NET thì đều có Garbage Collection sẵn, lợi ích đó là dev không còn phải quan tâm tới vấn đề giải phóng khi không dùng tới nữa, mặt trái của nó là làm chậm chương trình 1 tí (tất nhiên rồi). Sau khi đã dọn xong thì bộ nhớ sẽ được phân bổ lại cho Heap.

Vậy Heap là gì?

Heap là đống.

Trong C/C++, vùng nhớ sẽ được phân như thế này:

alt

Text segment
Là nơi mà lưu trữ code đã được compile. Các yếu tố bên ngoài không thể can thiệp trực tiếp đến phân vùng này. Khi chạy chương trình thì việc đầu tiên chính là compile code vào text segment.

Initialized data segment
Là phân vùng sử dụng để khởi tạo giá trị cho static variable và global variable.

Uninitialized data segment
Cũng giống như Initialized data segment nhưng dành cho static variable và global variable chưa gán giá trị cụ thể.

Stack segment
Được dùng để cấp phát bộ nhớ cho function parameters và local variables.

Heap segment
Được sử dụng để cấp phát bộ nhớ bằng kĩ thuật dynamic memory allocation.
Đây cũng chính là nơi V8 cấp phát bộ nhớ để tạo mới object.

Để cấp phát bộ nhớ động trong C++ cũng khá dễ, ví dụ:

int *idolAge = new int;
int *listIdol = new int[10];

Đặc điểm của Heap là bộ nhớ được cấp phát sẽ không tự giải phóng cho đến khi nào toàn bộ chương trình kết thúc, nên trong quá trình chạy thì sẽ có thể dư thừa và gây lãng phí bộ nhớ.

Đối với V8, nó có một điểm đặc biệt đó là nó sẽ chia Heap thành 2 phần: new space - nơi các object được tạo mới và old space - nơi các object vẫn còn giá trị được lại sau khi quét rác.

alt text

Trong quá trình gom rác, nó sẽ tạm thời ngừng thực thi khi quá trình gom rác thực hiện, tuy nhiên để giảm thiểu tác động này thì nó sẽ chỉ xử lí một phần của object heap trong hầu hết chu kì gom rác, và trong quá trình gom thì V8 luôn biết chính xác nơi mà tất cả các object và pointer nằm trong bộ nhớ, giúp ta tránh được memory leaks.

Vào đầu năm nay thì phiên bản 5.9 của V8 vừa ra mắt đã có một sự thay đổi lớn, đó là Ignition và TurboFan.

alt

Tổng quan

Ignition và TurboFan ra đời để tối ưu hóa tốc độ cũng như thay thế những thiếu sót của 2 người tiền nhiệm là Full-codegen và Crankshaft.

TurboFan được thiết kế như là một compiler cho phép tách biệt giữa tối ưu hóa compiler cấp cao và cấp thấp, khiến cho việc thêm các tính năng mới vào Javascript mà không cần phải sửa đổi mã kiến trúc. Còn với Ignition, nhìn chung lí do mà nó ra đời chính là do việc giảm bộ nhớ tiêu thụ trên điện thoại. Trước đó khi còn sử dụng Full-codegen, code được compiler này compile chiếm khoảng 1/3 Heap trong Chrome, khiến cho dung lượng còn lại là quá ít cho web.

alt

Sử dụng Ignition, V8 sẽ thông dịch code thành bytecode, bộ nhớ cấp phát của nó chỉ khoảng một nửa so với cách ở trên. Sau đó V8 tận dụng bytecode có được để TurboFan generate trực tiếp ra machine code thay vì phải biên dịch lại từ mã nguồn như Crankshaft đã làm.

alt
Benchmarks giữa 5.8 và 5.9

V8 Bytecode

Khi V8 biên dịch mã JavaScript, trình phân tích cú pháp (parsers) sẽ generate một cây cú pháp trừu tượng (abstract syntax tree). Ignition sẽ generate bytecode từ syntax tree. TurboFan sẽ lấy mã bytecode và generate machine code tối ưu từ nó.

alt

Dịch bytecode về machine code sẽ đơn giản hơn nếu bytecode được thiết kế cùng mô hình như CPU (mỗi dòng CPU có một tập lệnh riêng).

alt

Trên là một vài nét cơ bản về Chrome V8, nhìn chung đây là một Javascript engine mạnh mẽ, open source, đáng để sử dụng cũng như những người đang lập trình Nodejs hiểu thêm về engine đằng sau nó.

Bài viết gốc: Vài nét về V8 - Javascript Engine đằng sau Chrome và Node.js

0