What makes WebAssembly fast?

Trong bài viết trước , chúng ta đã tìm hiểu cách tạo và làm việc với WebAssembly modules. Ở bài viết này, chúng ta sẽ tìm hiểu tại sao mà WebAssembly lại chạy nhanh hơn so với JavaScript.

What does JavaScript performance look like today?

Trước khi chúng ta có thể hiểu được sự khác biệt về hiệu năng giữa JavaScript và WebAssembly, chúng ta cần phải hiểu được những công việc mà JS engine thực hiện. Biểu đồ này đưa ra một bức tranh sơ bộ về qúa trình khởi động của một ứng dụng. Mỗi thanh sẽ hiển thị thời gian thực hiện một nhiệm vụ cụ thể.

  • Parsing : thời gian cần thiết để xử lý source code thành thứ mà trình thông dịch (interpreter) có thể chạy.
  • Compiling + optimizing : thời gian cho trình biên dịch baseline và tối ưu hóa. Một số công việc tối ưu hóa của trình biên dịch không phải là các chủ đề chính, vì vậy nó không được đưa vào đây.
  • Re-optimizing : thời điểm JIT dành để điều chỉnh khi các giả định của nó đã failed, cả code đang re-optimizing và bailing out of optimized sẽ quay trở về baseline code.
  • Execution : thời gian cần thiết để thực thi code.
  • Garbage collection : thời gian dọn dẹp bộ nhớ.

Một điều quan trọng cần lưu ý: những công việc này không xảy ra trong khối rời rạc hoặc trong một chuỗi cụ thể. Thay vào đó, chúng sẽ được thực thi xen kẽ. Một số sẽ được phân tích, sau đó một số được thực thi, sau đó một số được biên dịch, sau đó một số được phân tích nhiều hơn, sau đó một số được thực thi nhiều hơn, vv Việc thực hiện theo chiến lược breakdown này mang lại một cải tiến lớn so với những ngày đầu của JavaScript, khi công việc được thực hiện như thế này: Ban đầu, khi đó chỉ là một trình thông dịch chạy JavaScript, các xử lý diễn ra khá chậm. Sau khi JITs được giới thiệu, nó đẩy nhanh tốc độ thực hiện một cách đáng kể. Nếu nhà phát triển JavaScript duy trì việc viết code JavaScript trong cùng một cách mà họ đã làm thì, thời gian phân tích cú pháp và biên dịch sẽ là rất nhỏ. Việc hiệu suất được cải thiện dẫn đến các nhà phát triển có thể tạo ra các ứng dụng JavaScript lớn hơn.

How does WebAssembly compare?

Fetching

Thời gian thực hiện công việc này không được thể hiện trong biểu đồ, nhưng có một điều đơn giản là việc lấy các tập tin từ máy chủ cũng chiếm một khoảng thời gian nhất định. Bởi vì WebAssembly nhỏ gọn hơn so với JavaScript, nên việc lấy nó tất nhiên sẽ nhanh hơn. Mặc dù các thuật toán nén có thể làm giảm đáng kể kích thước của một JavaScript bundle, nhưng các file sau khi nén của WebAssembly vẫn nhỏ hơn. Điều này có nghĩa phải mất ít thời gian hơn để chuyển nó giữa server và client. Điều này đặc biệt có ích trong môi trường mạng chậm.

Parsing

Một khi đã đi đến trình duyệt, source code JavaScript được phân tách thành Abstract Syntax Tree (AST). Các trình duyệt thường làm điều này một cách lười biếng, chỉ phân tích những gì chúng thực sự cần lúc đầu và chỉ tạo ra stubs cho các function chưa được gọi ra. Từ đó, AST được chuyển thành một đại diện trung gian (gọi là bytecode) cụ thể phù hợp cho các JS engine. Ngược lại, WebAssembly không cần phải đi qua sự chuyển đổi này vì nó đã là một đại diện trung gian. Nó chỉ cần được giải mã và xác nhận để đảm bảo không có bất kỳ sai sót nào trong nó.

Compiling + optimizing

Như đã giải thích trong bài viết về JIT , JavaScript được biên dịch trong quá trình thực thi code. Tùy thuộc vào những loại được sử dụng trong thời gian chạy, có thể nhiều phiên bản của cùng một đoạn code cần phải được biên dịch. Trình duyệt khác nhau xử lý việc biên dịch WebAssembly khác nhau. Một số trình duyệt tạo ra baseline compilation của WebAssembly trước khi bắt đầu thực thi nó, và một số khác sử dụng JIT. Dù bằng cách nào, các mã WebAssembly cũng gần gũi hơn với mã máy. Và nó nhanh hơn do một vài lý do sau đây:

  1. Trình biên dịch không phải mất thời gian thực thi code để phát hiện những loại đang được sử dụng trước khi nó bắt đầu biên dịch code đã được tối ưu hóa.
  2. Trình biên dịch không phải biên dịch ra các phiên bản khác nhau của cùng một đoạn code dựa trên các loại khác nhau.
  3. Việc tối ưu hóa sâu hơn đã được thực hiện trước thời hạn trong LLVM. Vì vậy, cần ít công việchơn để biên dịch và tối ưu hóa nó.

Re-optimizing

Đôi khi JIT có để ném ra một phiên bản đã tối ưu hóa của code và thử lại nó. Điều này xảy ra khi giả định rằng JIT làm cho dựa trên code đang chạy được bật ra không chính xác. Ví dụ, deoptimization xảy ra khi các biến đi vào một vòng lặp khác với chúng ở những lần lặp lại trước đo, hoặc khi một function mới được chèn trong chuỗi ban đầu. Có hai chi phí của việc deoptimization. Đầu tiên, phải mất một thời gian để giải thoát code đã tối ưu hóa và quay trở lại với phiên bản ban đầu. Thứ hai, nếu function đó đang được gọi nhiều lần, JIT có thể quyết định gửi nó qua trình tối ưu hóa biên dịch một lần nữa, do đó mất chi phí biên dịch nó lần thứ hai. Trong WebAssembly, những thứ như type là rõ ràng, vì vậy JIT không cần phải thực hiện các giả định về các type dựa trên dữ liệu mà nó thu thập được trong thời gian chạy. Điều này có nghĩa nó không phải trải qua chu kỳ reoptimization.

Executing

Có thể viết Javascript thực thi một cách có hiệu năng cao. Để làm điều đó, bạn cần phải biết về các việc tối ưu hóa mà JIT làm. Ví dụ, bạn cần phải biết viết mã thế nào để trình biên dịch có thể type specialize nó, như được giải thích trong bài viết trên JIT . Tuy nhiên, hầu hết các nhà phát triển không biết về JIT internal. Ngay cả đối với những nhà phát triển biết về JIT internals, có thể họ cũng rất khó khăn để làm được điều trên. Thêm vào đó, tối ưu hóa sử dụng JIT là khác nhau giữa các trình duyệt, do đó một đoạn code có thể là tối ưu hóa cho trình duyệt này, nhưng lại không tối ưu hóa ở trình duyệt khác. Bởi vì điều này, thực thi mã trong WebAssembly thường nhanh hơn. Bên cạnh đó, WebAssembly được thiết kế như một mục tiêu biên dịch. Điều này có nghĩa nó được thiết kế cho các trình biên dịch để tạo ra, chứ không phải cho các lập trình viên con người viết. Kể từ khi lập trình viên không cần phải lập trình trực tiếp, WebAssembly có thể cung cấp một bộ các hướng dẫn lý tưởng hơn cho máy. Tùy thuộc vào loại công việc mà code của bạn đang thực hiện, các hướng dẫn chạy bất cứ nơi nào nhanh hơn từ 10% đến 800%.

Garbage collection

Trong JavaScript, nhà phát triển không phải lo lắng về việc giải phóng các biến cũ khỏi bộ nhớ khi không dùng đến chúng nữa. Thay vào đó, JS engine tự động sử dụng một cái gì đó gọi là Garbage Collector. Đây có thể là một vấn đề nếu bạn muốn kiểm soát được hiệu năng của code. Bạn không thể kiểm soát được công việc của garbage collector, vì vậy nó có thể đến vào một thời điểm không phù hợp. Ít nhất là cho đến bây giờ, WebAssembly không hỗ trợ việc thu gom biến thừa ở tất cả. Bộ nhớ được quản lý bằng tay (vì nó là các ngôn ngữ như C và C ++). Trong khi điều này có thể làm cho việc viết chương trình trở nên khó khăn hơn với các nhà phát triển, nhưng nó cũng làm cho hiệu suất ổn định hơn.

Conclusion

WebAssembly là nhanh hơn so với JavaScript trong nhiều trường hợp vì:

  • fetching WebAssembly mất ít thời gian vì nó nhỏ gọn hơn so với JavaScript, ngay cả khi nén.
  • giải mã WebAssembly mất ít thời gian hơn so với phân tích cú pháp JavaScript.
  • biên dịch và tối ưu hóa mất ít thời gian vì WebAssembly gần gũi với mã máy hơn so với JavaScript và đã đã trải qua tối ưu hóa trên phía máy chủ.
  • reoptimizing không cần phải xảy ra vì WebAssembly có các loại và các thông tin khác được built bên trong, do đó JS engine không cần phải suy đoán khi tối ưu hóa như cách nó vẫn làm với mã JavaScript.
  • executing thường mất ít thời gian hơn vì có ít thủ thuật biên dịch và gotchas rằng các nhà phát triển cần biết để viết mã một cách nhất quán performant, cộng với việc bộ hướng dẫn WebAssembly lý tưởng hơn cho máy.
  • thu gom rác thải không được yêu cầu do bộ nhớ được quản lý bằng tay.

Đây là lý do tại sao, trong nhiều trường hợp, WebAssembly sẽ làm tốt hơn JavaScript khi thực hiện các nhiệm vụ tương tự. Có một số trường hợp WebAssembly không thực hiện đúng như mong đợi, và cũng có một số thay đổi trên horizon sẽ làm cho nó nhanh hơn. Chúng ta sẽ tìm hiểu những điều đó trong bài viết tiếp theo.