Đang thực hiện
Tên đăng nhập
Mật khẩu
 
Hoặc đăng nhập bằng:
Nhập lại mật khẩu

Trang chủ Tin tổng hợp
Tin tổng hợp

Trình biên dịch Just-in-time

Cập nhật: 30/08/2018 Lượt xem: 255
JIT (Just-in-time). Nó làm cho JavaScript chạy nhanh hơn bằng cách theo dõi mã khi nó chạy và gửi đường dẫn mã hot được tối ưu hóa. Điều này đã dẫn đến cải tiến hiệu suất nhiều...

Như một cách để loại bỏ phần không hiệu quả của trình thông dịch — nơi mà trình thông dịch phải tiếp tục truyền lại mã mỗi khi chúng đi qua vòng lặp — trình duyệt bắt đầu trộn các trình biên dịch vào.

Trình duyệt khác nhau làm điều này cũng theo cách khác nhau, nhưng về cơ bản thì ý tưởng là như nhau. Họ đã thêm một phần mới vào
 JavaScript engine, được gọi là một monitor (hay còn gọi là profiler). Monitor đó đọc code khi nó chạy, và ghi lại số lần nó chạy và loại nào được phép sử dụng.

Lúc đầu, monitor chỉ chạy mọi thứ thông qua trình thông dịch.
 

trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-1

Nếu cùng một dòng code được chạy một vài lần, đoạn code đó được gọi là ấm (warm). Nếu nó chạy rất nhiều, thì nó được gọi là nóng (hot).

Baseline compiler


 
Khi một hàm (function) bắt đầu trở nên ấm, JIT sẽ gửi nó đi để được biên dịch. Sau đó, nó sẽ lưu trữ biên dịch đó.

 
 
trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-2
 

Mỗi dòng của hàm được biên dịch thành một “stub”. Stub được lập chỉ mục theo số dòng và loại biến (Tôi sẽ giải thích lý do tại sao điều đó quan trọng sau này). Nếu monitor thấy chương trình đang thực thi cùng một đoạn code một lần nữa với các kiểu biến tương tự, nó sẽ chỉ kéo ra phiên bản đã biên dịch của nó và chạy. 

Điều đó giúp tăng tốc mọi thứ. Nhưng như tôi đã nói, có nhiều thứ hơn mà trình biên dịch có thể làm. Có thể mất chút thời gian để tìm ra cách hiệu quả nhất để làm mọi thứ… để tối ưu hóa. 

Trình biên dịch cơ sở sẽ thực hiện một số tối ưu hóa này (tôi đưa ra ví dụ về một bên dưới). Tuy nhiên, không muốn mất quá nhiều thời gian, bởi vì nó không muốn thực hiện quá lâu. 

Tuy nhiên, nếu
code thực sự hot - nếu mã đang chạy toàn bộ thời gian — thì bạn nên dành thêm thời gian để tối ưu hóa thêm.
 

Optimizing compiler

 

Khi một phần của code đang rất hot, monitor sẽ gửi nó tới trình biên dịch tối ưu hóa. Điều này sẽ tạo ra một phiên bản khác của function cũng sẽ được lưu trữ.

 
trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-3

Để tạo ra một phiên bản chương trình nhanh hơn, trình biên dịch tối ưu hóa phải đưa ra một số giả định. 

Ví dụ: Nếu có thể giả định rằng tất cả các đối tượng (object) được tạo bởi một hàm (function) constructor có khung giống nhau - tức là chúng luôn có cùng tên thuộc tính và các thuộc tính đó được thêm vào theo thứ tự - nó có thể dùng cắt một phần dựa trên đó.. 

Optimizing compiler sử dụng thông tin mà monitor đã thu thập bằng cách xem code 
thực thi để thực hiện các bản dịch này. Nếu một cái gì đó đã đúng cho tất cả các lần truyền trước đó thông qua một vòng lặp, nó sẽ tiếp tục giả định là đúng. 

Nhưng tất nhiên, với JavaScript, không bao giờ có bất kỳ đảm bảo nào. Bạn có thể có 99 objects mà tất cả đều có cùng một khung, nhưng sau đó object thứ 100 có thể sẽ thiếu một thuộc tính. 

Vì vậy, code được biên dịch cần phải kiểm tra trước khi nó chạy để xem liệu các giả định có hợp lệ hay không. Nếu có, thì
code  được biên dịch sẽ chạy. Nếu không, JIT giả định rằng nó đã đưa ra các giả định sai và loại bỏ Optimizing compiler.
 
trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-4

Sau đó, thực hiện quay trở lại thông dịch (interpreter) hoặc phiên bản được biên dịch (baseline compiled) cơ sở. Quá trình này được gọi là deoptimization (hoặc bailing ra). 

Thông thường
Optimizing compilers làm cho code nhanh hơn, nhưng đôi khi chúng có thể gây ra các vấn đề không mong muốn về hiệu suất. Nếu bạn có code mà nó tiếp tục được tối ưu hóa và sau đó deoptimized, nó sẽ kết thúc chậm hơn so với chỉ thực hiện các phiên bản biên dịch cơ sở. 

Hầu hết các trình duyệt đều đã thêm giới hạn để thoát ra khỏi các chu kỳ tối ưu hóa / deoptimization này khi chúng xảy ra. Nếu JIT đã thực hiện nhiều hơn, điều đó nói cho JIT rằng, nó cần phải dừng lại.

 

Ví dụ về tối ưu hóa: Type specialization



Có rất nhiều loại tối ưu hóa khác nhau, nhưng tôi muốn xem xét một loại để bạn có thể cảm nhận được cách tối ưu hóa đã diễn ra. Một trong những chiến thắng lớn nhất trong việc tối ưu hóa các trình biên dịch xuất phát từ một cái gì đó gọi là chuyên môn hóa kiểu (Type specialization). 

Hệ thống kiểu động mà JavaScript sử dụng đòi hỏi một chút công việc phụ khi chạy, ví dụ: Hãy xem xét đoạn code dưới đây:


function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
     sum += arr[i];
  }
}



Bước + = trong vòng lặp trông có vẻ đơn giản. Nó có vẻ như bạn có thể tính toán điều này trong một bước, nhưng vì gõ động, khiến bạn phải mất nhiều bước hơn mong đợi. 

Giả sử rằng arr là một mảng gồm 100 số nguyên. Khi hàm khởi động, trình biên dịch Baseline sẽ tạo ra một nhánh cho mỗi thao tác trong hàm. Vì vậy, sẽ có một sơ khai cho sum + = arr [i], sinh ra để xử lý thao tác + =. 

Tuy nhiên, sumarr [i] không được đảm bảo là số nguyên. Bởi vì kiểu động trong JavaScript, có khả năng là trong vòng lặp sau của vòng lặp, arr [i] sẽ là một chuỗi. Số nguyên và chuỗi nối là hai thứ hoạt động rất khác nhau, do đó, chúng sẽ biên dịch thành mã máy rất khác nhau. 

Cách JIT xử lý điều này là bằng cách biên dịch nhiều stub. Nếu một đoạn mã đơn lẻ (có nghĩa là, luôn luôn được gọi với cùng một loại) nó sẽ nhận được một
stub. Nếu nó có tính đa hình (được gọi với các kiểu khác nhau từ một lần truyền qua mã này đến mã khác), thì nó sẽ nhận được một nhánh cho mỗi kết hợp các kiểu đã đi qua thao tác đó. 

Điều này có nghĩa là JIT phải hỏi rất nhiều câu hỏi trước khi nó chọn một stub.


 
 
trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-5
 
Bởi vì mỗi dòng mã có bộ sơ đồ riêng của nó trong trình biên dịch Baseline, JIT cần phải tiếp tục kiểm tra các loại mỗi khi dòng mã được thực thi. Vì vậy, đối với mỗi lần lặp qua vòng lặp, nó sẽ phải lặp lại cùng một câu hỏi.
 
trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-6

Chương trình sẽ thực thi nhanh hơn rất nhiều nếu JIT không cần lặp lại các câu hỏi đó. Và đó là một trong những thứ mà trình biên dịch tối ưu hóa (optimizing compiler) thực hiện.

Trong trình biên dịch tối ưu hóa, toàn bộ hàm được biên dịch cùng nhau. Các loại kiểm tra được di chuyển để chúng phải xảy ra trước khi vòng lặp (Loop) được thực hiện.


 
trinh-bien-dich-just-in-time-trinh-bien-dich-tot-nhat-the-gioi-7

Một số JITs còn tối ưu hóa điều này hơn nữa.

Ví dụ:

Trong Firefox, có một phân loại đặc biệt cho các mảng chỉ chứa số nguyên. Nếu
arr là một trong các mảng này, thì JIT không cần kiểm tra xem arr [i] có phải là một số nguyên hay không. Điều này có nghĩa rằng JIT có thể thực hiện tất cả các kiểu kiểm tra trước khi nó đi bắt đầu đi vào vòng lặp.

 

Kết luận



Đó là chính là JIT (Just-in-time). Nó làm cho JavaScript chạy nhanh hơn bằng cách theo dõi mã khi nó chạy và gửi đường dẫn mã hot được tối ưu hóa. Điều này đã dẫn đến cải tiến hiệu suất nhiều lần cho hầu hết các ứng dụng JavaScript. 

Tuy nhiên, ngay cả với những cải tiến này, hiệu suất của JavaScript có thể không thể đoán trước được. Và để làm cho mọi thứ nhanh hơn, JIT đã bổ sung thêm một thứ trong khi chạy, bao gồm: 

- Tối ưu hóa (optimization) và deoptimization 
- Bộ nhớ được sử dụng cho monitor's bookkeeping và recovery infomation khi bailouts xảy ra 
- Bộ nhớ được sử dụng để lưu trữ baseline và tối ưu hóa các phiên bản của một function
 

Bỏ qua phần bên trên. Chúng ta hãy hỏi "Làm thế nào hiệu suất có thể dự đoán trước được?"

Đó là giải pháp của Assembly.

Trong bài viết tiếp theo, tôi sẽ giải thích thêm về Assembly và cách trình biên dịch làm việc với nó.



 
Tư vấn viên 1: Lê Thoa
Tư vấn viên 2: Thu Huyền
Tuyển sinh lập trình viên quốc tế - MMS new vision
Khóa học C&B Excel - Trần Văn Hải