Bài 2: Nghiên cứu mô hình dịch của TypeScript, JavaScript
1. Mô hình dịch của TypeScript
Bài trước chúng ta được giới thiệu qua về TypeScript, bản chất của TypeScript thì vẫn là JavaScript, chức năng của TypeScript cũng chỉ là để biên dịch về JavaScript, nó không phải là một ngôn ngữ có thể vận hành độc lập và đồng thời cũng không thể thay thế được vai trò của JavaScript. Vì vậy mà chúng ta sẽ tập trung nghiên cứu về mô hình dịch của JavaScript nhé 🔥.
Mối quan hệ giữa TypeScript và Javascript
2. Mô hình dịch của JavaScript
Nếu anh em là một JS developer hay đã từng học về JavaScript thì rất có khả năng đã từng nghe thấy “V8”. Cũng là V8 nhưng mà nó lạ lắm 😂 anh em khác lại tưởng bọn JS developer giàu nứt đố đổ vách, cứ mở “khẩu” ra là bọn nó lại nói chuyện về xe với chả pháo. Nhưng V8 mà chúng ta nhắc đến ở đây không phải là loại động cơ trên xe ô tô đâu anh em nhé 🤣.
Vậy thì trong bài viết này chúng ta sẽ cùng nhau nghiên cứu sâu về cách mà V8 engine và cách nó hoạt động như thế nào nhé. Let’s go!
3. V8 là gì?
V8 engine là phần mềm mã nguồn mở (open source) được xây dựng bởi Google và được viết bằng C++. Bộ engine này được dùng trong trình duyệt Google Chrome. Không giống như các engine khác, V8 còn được sử dụng trong môi trường runtime (môi trường thực thi) của Node.js.
4. Ngoài V8 ra còn loại JS engine nào không?
Ngoài V8 engine được phát triển bởi Google ra thì còn rất nhiều các JavaScript engine khác được sử dụng trong các dự án phổ biến như:
- Rhino – Được quản lý bởi quỹ Mozilla Foundation, nguồn mở, phát triển hoàn toàn bằng Java
- SpiderMonkey – Bộ JS engine đầu tiên, ngày xưa được hỗ trợ bởi Netscape Navigator, ngày nay là Firefox
- JavaScriptCore – nguồn mở, còn được gọi là Nitro, được phát triển bởi Apple cho Safari
- KJS – Engine của KDE, phát triển bởi Harri Porten cho dự án trình duyệt Konqueror của KDE
- Chakra (JScript9) – Internet Explorer
- Chakra (JavaScript) – Microsoft Edge
- Nashorn – Nguồn mở và là 1 phần của OpenJDK, viết bằng Java bởi Oracle và Tool Group
- JerryScript – là 1 bộ engine nhẹ dành cho IOT (Internet of Things
5. Đả qua một vài thông tin lịch sử
Google tạo ra Google Maps sử dụng trên trình duyệt, tuy nhiên sản phẩm này của Google đòi hỏi rất nhiều về sức mạnh xử lý. Việc triển khai JavaScript tại thời điểm đó không đủ tốt để chạy Google Maps nhanh. Đương nhiên là một ông lớn – Big Tech, Google sẽ làm mọi cách để thu hút được thật nhiều người dùng tiếp cận tới các sản phẩm & dịch vụ của họ. Vì vậy mà sản phẩm Google cung cấp ra cần phải có tốc độ xử lý nhanh và thật mạnh mẽ, về vấn đề này Google đã bắt tay vào xây dựng công cụ của riêng mình là V8 engine. V8 đầu tiên được thiết kế nhằm gia tăng hiệu suất của tiến trình thực thi JavaScript bên trong trình duyệt. Được ra mắt vào năm 2008 và đạt được tốc độ xử lý khá nhanh, chắc có thể coi là nhanh nhất rồi 😂.
6. Trái tim của V8 engine:
Trước khi đi cụ thể vào V8, đầu tiên chúng ta sẽ cần hiểu được một JS engine nó hoạt động như thế nào?
Với bức tranh tổng quan mà JS engine làm việc chúng ta thấy được những phần chính sau:
+ Parser:
Ban đầu code JavaScript sẽ đi qua công cụ được coi là một trình phân tích cú pháp, nó sẽ thực hiện phân tích thành từng tokens để xác định từng ý nghĩa, vai trò của chúng.
+ AST (Abstract Syntax Tree):
Với những tokens từ parser, sẽ tạo ra AST (cây cú pháp trừu tượng). AST đóng vai trò rất quan trọng trong việc phân tích ngữ nghĩa, là nơi mà trình biên dịch (Interpreter) xác nhận tính đúng đắn và cách sử dụng phù hợp của chương trình cũng như các phần tử của ngôn ngữ.
Đả qua tẹo về AST:
Đừng phân tâm ở chỗ này nhé anh em, vấn đề đang nói đến vẫn phải là bức tranh tổng quát của JS engine đó 😀:
Thực tế mà nói, bước đầu tiên trong tất cả các quy trình biên dịch của hầu hết mọi ngôn ngữ hiện có là tạo ra được cây cú pháp trừu tượng. AST là một biểu diễn dạng cây cho cấu trúc cú pháp của một mã nguồn nhất định. Về mặt lý thuyết, AST có thể được dịch sang bất kỳ ngôn ngữ nào khác. Mỗi node của cây biểu thị một cấu trúc ngôn ngữ xuất hiện trong source code.
Đối với JS engine, mỗi dòng code JavaScript sẽ được chuyển đổi thành AST, ví dụ:
Đoạn code JavaScript: const title = “ACD Academy”;
Được chuyển thành AST:
Ngoài ra anh em có thể nghiên cứu thêm về các công cụ chuyển đổi code từ một ngôn ngữ lập trình sang cây cú pháp nhé.
Ví dụ: Convert code JavaScript sang AST trên astexplorer
+ Interpreter:
Sau khi AST được tạo ở bước trước đó, nó sẽ được cung cấp cho trình thông dịch tạo ra mã máy không được tối ưu hóa một cách nhanh chóng và quá trình thực thi có thể bắt đầu ngay lập tức.
+ Profiler:
Một tiến trình Profiler sẽ theo dõi và cảnh báo những phần mã nào đang chiếm dụng nhiều tài nguyên và thời gian xử lý để có biện pháp tối ưu chúng.
+ Compiler:
Dưới sự giám sát của Profiler, những đoạn code nào còn chưa được tối ưu sẽ được chuyển cho trình biên dịch (Complier) cải tiến và tạo ra mã máy.
+ Optimized code
Sau khi được xử lý qua các bước trên, sẽ nhận được đoạn code đã được tối ưu.
4.1 Phiên bản ra đời trước V8 5.9
Để có thể đạt được tốc độ tốt, V8 dịch mã JavaScript thành mã máy (machine code) thay vì sử dụng trình thông dịch. Nó biên dịch mã JavaScript thành mã máy ngay khi thực thi bằng bộ JIT compiler (Just-In-Time compiler) giống như đa số các JS engine hiện đại khác như SpiderMoney hay Rhino. Điểm khác biệt chính đó là V8 không sinh ra bytecode hay mã trung gian.
Trước khi phát hành phiên bản V8 5.9 (đầu năm 2017), V8 từng sử dụng 2 trình biên dịch:
full-codegen: là một trình biên dịch làm việc khá nhanh để có thể sinh ra được mã máy đơn giản. Chính vì nó sinh ra mã máy rất nhanh nên cũng không thể đòi hỏi code tối ưu được do vậy mà code có chạy thì đương nhiên là é* nhanh được rồi 😝.
Crankshaft: là một trình biên dịch phức tạp hơn (JIT hay Just-In-Time) sinh ra mã được tối ưu tốt hơn.
Khi thực thi code JavaScript lần đầu tiên, V8 engine sẽ sử dụng sức mạnh của full-codegen để dịch trực tiếp những đoạn code JavaScript đã được phân tích cú pháp (AST) thành mã máy (machine code) mà không cần qua bất kì bước chuyển đổi nào (transformation).
Do vậy mà mã máy được sinh ra và được thực thi rất nhanh. Chỗ full-codegen này anh em cũng lưu ý một điêu rằng, V8 không hề sử dụng bytecode làm trung gian, điều đó cho thấy rằng với cách làm này hoàn toàn đã loại bỏ một sự không cân thiết của một trình thông dịch (Interpreted).
Tuy nhiên thì các tiêu chuẩn dần thay đổi, vấn đề về kỹ thuật tăng lên và Smartphones trở nên phổ biến hơn so với Desktops. Ở thời điểm này thì V8 của Chrome đã tiêu tốn quá nhiều RAM và không còn có thể bắt kịp với các tính năng mới của ngôn ngữ JavaScript (ES2015) và tối ưu hóa các tính năng đó. Nếu anh em còn nhớ thì chúng ta cũng không ít lần bắt gặp những hình ảnh hài hước trên mạng xã hội nói về “chuyện tình” Chrome và RAM 😂.
Hình ảnh funny về Chrome và RAM (2010-2015)
4.2 Phiên bản V8 5.9
Phiên bản V8 5.9, full-codegen và Crankshaft không còn được dùng trong V8 để thực thi JavaScript. Với việc phát hành phiên bản V8 5.9, thì V8 engine của lúc bấy giờ cũng đã có một quy trình thực thi mới (new execution pipeline) – mới như thế nào thì chúng ta hãy dành ra ít phút để hiểu thêm về sự “giao thời” 🎉 trước khi có V8 5.9 đã nhé 🤠.
Với Ignition, V8 biên dịch các hàm JavaScript thành bytecode ngắn gọn, có kích thước từ 50% đến 25% so với mã máy (machine code) ở quy trình baseline sinh ra (quan sát ảnh ngay trên nhé anh em, có 3 phần trong pipeline là Interpreted – Baseline – Optimized). Bytecode này sau đó sẽ được thực thi bởi trình thông dịch có hiệu suất cao để mang lại tốc độ thực thi nhanh chóng trên các trang web trong thế giới thực, tốc độ đạt được gần với tốc độ thực thi của mã máy do trình biên dịch cơ sở (Baseline) hiện có của V8 tạo ra.
Trong Chrome 53, Ignition sẽ được bật cho các thiết bị Android có RAM hạn chế (512 MB trở xuống), là những thiết bị cần tiết kiệm bộ nhớ nhất. Kết quả từ các thử nghiệm ban đầu cho thấy Ignition giảm khoảng 5% bộ nhớ của mỗi tab Chrome 😱.
Phần Optimized xuất hiện thêm TurboFan 🤔, lý do là vì với sự xuất hiện của các tính năng JavaScript (hiện đại) – Crankshaft không thể xử lý tối ưu hóa chúng được. Những kẽ hở này được coi là performance cliffs – hiểu đơn giản là đôi khi đoạn code đang chạy nhanh, nó rơi vào Crankshaft mà bản thân một phần đoạn code đó sử dụng những tính năng mới của JS mà Crankshaft nó không thể xử lý tối ưu chỗ code đó và sau cùng nó đùn ra trả về đoạn code còn “thúi” hơn cả code ban đầu bỏ vào 😂. Cho nên với những chỗ như vậy cần được đi qua TurboFan.
Tuy nhiên cách kết hợp này lại dẫn đến rất nhiều vấn đề, đặc biệt là khi xem xét việc tối ưu hóa nào cần được áp dụng nhất quán cho trình biên dịch. Nó cũng giống việc anh em đi ra đường, nhìn thấy em nào cũng xinh, thậm chí có anh em còn đòi đi tù cho bằng được 🤣, nếu như được chọn một em để làm vợ đôi khi cũng không biết mình thực sự nên chọn em nào. Mỗi lần hỏi có khi lại cho ra một câu trả lời khác nhau, ảo thật đấy 🤑. Với pipeline của V8 lúc đó cũng khó đoán như vậy đó, ngoài ra vấn đề này cũng gây ra việc hao tâm tổn sức cho việc tích hợp các công cụ – DevTools như debug, profiler,… Nhận thấy pha xử lý cho sự kết hợp này khá phức tạp và cồng kềnh nên V8 5.9 đã lược bỏ đi full-codegen và Crankshaft.
Ok, bây giờ sẽ trả lại sân khấu cho V8 5.9 🤩, ý tưởng xuất phát từ mô hình ưu tiên thiết bị di động (mobile first) với trọng tâm là giảm mức tiêu thụ bộ nhớ của V8, nghĩa là không chỉ về thiết kế những gì mà người dùng nhìn thấy mà còn cả về kiến trúc phần mềm để phù hợp với thiết bị di động. Đó là trọng trách mà V8 engine sẽ phải giải quyết, bài toán mang tên tối ưu hóa việc sử dụng bộ nhớ và CPU trên các thiết bị có dung lượng bộ nhớ nhỏ và bộ vi xử lý thiếu mạnh mẽ, chẳng hạn như các thiết bị di động sử dụng hệ điều hành Android.
Ok vậy new execution pipeline của phiên bản V8 5.9 mang tới có những gì 🤔? V8 5.9 có 2 pipeline thực thi mới được ra mắt, chúng mang lại khả năng cải thiện hiệu năng tốt hơn và tiết kiệm bộ nhớ đáng kể đối với những ứng dụng sử dụng JavaScript. Bộ pipeline mới này được xây dựng trên trình thông dịch của V8 là Ignition và trình biên dịch tối ưu hóa mới nhất – TurboFan.
Chúng ta sẽ xem Ignition-TurboFan pipeline trong V8 5.9 hoạt động bằng hình minh họa dưới đây:
Ignition: là một trình thông dịch. Đả qua sơ yếu lý lịch của nó tẹo :)) Một trong những vấn đề mà V8 phải đối mặt (ngoài sự phức tạp về kiến trúc) là mã máy (machine code) sau khi được sinh ra có thể tiêu tốn một lượng bộ nhớ đáng kể, ngay cả khi những mã này chỉ được thực thi một lần.
Để giảm thiểu chi phí này, nhóm V8 đã xây dựng một trình thông dịch JavaScript mới, được gọi là Ignition, có thể thay thế trình biên dịch cơ bản của V8, thực thi mã với ít chi phí bộ nhớ hơn và mở đường cho một quy trình thực thi tập lệnh đơn giản hơn.
Chắc mổ xẻ thêm tẹo nữa để hiểu rõ hơn 😅, Ignition không phải là trình phân tích cú pháp, nó đơn giản chỉ là trình thông dịch bytecode, có nghĩa là mã được đọc vào ở dạng bytecode và được cho ra (outputted) cũng ở dạng bytecode. Về cơ bản, những gì mà Ignition thực hiện là lấy nguồn bytecode và tối ưu hóa nó để tạo ra bytecode có kích thước nhỏ hơn rất nhiều và một trong những công việc mà nó làm là cũng loại bỏ cả những code không được sử dụng.
Cụ thể: Sau khi đi qua AST, bytecode được tạo (generated), từng mã một sẽ được đưa vào đường dẫn tối ưu hóa. Một số kỹ thuật tối ưu hóa như: Register Optimisation, Peephole Optimisations và Dead-code Elimination. Quy trình tối ưu hóa diễn ra tuần tự, giúp Ignition có thể đọc bytecode nhỏ hơn và thông dịch bytecode được tối ưu hóa hơn.
TurboFan: là một trình biên dịch tối ưu hóa mới của V8. TurboFan có kiến trúc phân lớp và được phân tách thành 3 layer riêng biệt: Frontend, Optimizing và Backend. Lớp Frontend chịu trách nhiệm nhận bytecode được cung cấp từ phản hồi do Ignition thu thập, lớp Optimizing chịu trách nhiệm tối ưu hóa code bằng trình biên dịch tối ưu hóa TurboFan.
Tất cả các tác vụ cấp thấp khác như low level optimisations (tối ưu hóa cấp thấp), scheduling (lập lịch) và code generator (ở cấp thấp này là tạo ra mã máy) cho các kiến trúc được hỗ trợ đều do lớp Backend xử lý – Ignition cũng dựa vào lớp Backend của TurboFan để tạo bytecode của nó. Chỉ riêng việc phân tách các lớp đã dẫn đến mã dành riêng cho máy ít hơn 29% so với trước đây.
Ngoài ra để nghiên cứu sâu hơn về các cách mà TurboFan sử dụng để optimizing, anh em có thể nghiên cứu sâu hơn qua các research papers sau:
Lời kết:
Bài viết đọc cũng tương đối mỏi mồm rồi :)) hi vọng với những thông tin vừa được nói đến trên, phần nào đã giúp các bạn có cái nhìn chính xác và chi tiết hơn về mô hình dịch của Typescript và JavaScript.
Thanks for reading!
Leave a Reply
Want to join the discussion?Feel free to contribute!