JavaScript là một ngôn ngữ thông dịch, nhưng các engine hiện đại sử dụng biên dịch JIT để tối ưu hóa hiệu suất. Trong JavaScript, mã nguồn không được biên dịch sang mã máy trước (ahead-of-time). Thay vào đó, mã được thông dịch từng dòng, nhưng khi chạy, engine sẽ thu thập thông tin và biên dịch những hàm “nóng” (chạy thường xuyên) thành mã máy tối ưu ngay tại thời điểm thực thi (“just in time”).
JIT compilation là phương pháp lai giữa trình thông dịch truyền thống và biên dịch Ahead-of-Time (AOT). Đây là cách hoạt động:
Quy trình JIT (Đơn giản hóa)
- Phân tích cú pháp (Parsing): Mã JS được phân tích cú pháp và chuyển đổi thành Cây Cú Pháp Trừu Tượng (AST).
- Thông dịch (Interpreter): AST được thực thi bởi một trình thông dịch bytecode (ví dụ: Ignition trong V8).
- Ghi hồ sơ (Profiling): Engine theo dõi những hàm/vòng lặp nào chạy thường xuyên (“mã nóng”).
- Bộ biên dịch JIT (JIT Compiler): Với mã nóng, JIT compiler (ví dụ: TurboFan trong V8 hoặc IonMonkey trong SpiderMonkey) sẽ chuyển bytecode thành mã máy tối ưu hóa, sử dụng thông tin thu thập được khi chạy.
- Hủy tối ưu hóa (Deoptimization): Nếu hành vi tại thời điểm chạy thay đổi (ví dụ: hình dạng của object thay đổi), engine có thể loại bỏ (“hủy tối ưu”) một số mã đã biên dịch và quay lại sử dụng thông dịch hoặc biên dịch lại với giả định mới.
Vai trò của Hidden Classes & Inline Caching trong JIT
Hai cơ chế này giúp tối ưu hóa việc truy cập thuộc tính trong các object JavaScript.
1. Hidden Classes (Hình dạng ẩn)
Object trong JavaScript là động — thuộc tính có thể được thêm/bỏ bất kỳ lúc nào, khiến việc truy cập thuộc tính không ổn định và chậm nếu engine phải “tìm kiếm” thuộc tính mỗi lần.
Giải pháp: Hidden Classes (còn gọi là "Shapes")
Khi một object JS được tạo, engine sẽ gán cho nó một “class ẩn” nội bộ dựa trên các thuộc tính của nó. Khi các thuộc tính được thêm theo cùng thứ tự ở nhiều object, engine sẽ tái sử dụng cùng một hidden class, giúp việc truy cập thuộc tính nhanh hơn nhờ vị trí cố định trong bộ nhớ (giống như struct trong C++).
function Foo() { this.x = 10; this.y = 20;
}
var a = new Foo();
var b = new Foo();
// Cả `a` và `b` đều dùng chung hidden class, nên việc truy cập `a.x` và `b.x` rất hiệu quả.
Nếu thêm thuộc tính theo thứ tự khác nhau, các hidden class khác nhau sẽ được tạo, làm giảm hiệu quả tối ưu:
var c = {};
c.x = 10;
c.y = 20; // Hidden class được tạo cho {x}, sau đó cập nhật thành {x, y} var d = {};
d.y = 20;
d.x = 10; // Hidden class được tạo cho {y}, sau đó cập nhật thành {y, x} (khác với `c`)
Hidden classes giúp JIT compiler sinh mã tối ưu cho truy cập thuộc tính, giả định rằng object có cấu trúc ổn định.
2. Inline Caching (Bộ đệm nội tuyến)
Khi truy cập một thuộc tính như obj.x, engine thường phải kiểm tra hidden class và duyệt qua chuỗi prototype — rất chậm nếu làm mỗi lần. Inline caching (IC) tối ưu việc tra cứu phương thức và thuộc tính bằng cách ghi nhớ mẫu truy cập trước đó.
Cách hoạt động của Inline Caching:
- Lần truy cập đầu tiên: Engine thực hiện tra cứu đầy đủ và ghi nhớ vị trí.
- Những lần sau: Engine tái sử dụng kết quả tra cứu nếu object có cùng hidden class.
Các kiểu gọi:
- Monomorphic: Tất cả object có cùng hidden class → truy cập nhanh nhất.
- Polymorphic: Có vài hidden class khác nhau → chậm hơn nhưng vẫn được tối ưu.
- Megamorphic: Nhiều hidden class → trượt cache, quay lại con đường chậm.
Tiếp tục với ví dụ và xem Inline Caching hoạt động:
// Hàm in giá trị 'x' từ một object.
function printX(obj) { console.log(obj.x);
} // Tạo hai object với cùng tên thuộc tính, thêm theo cùng thứ tự.
var a = { x: 1 };
var b = { x: 2 }; // Hai lời gọi sau:
// - Engine thấy cả 'a' và 'b' có cùng hidden class (cùng shape: {x})
// - Lời gọi đầu tiên được thông dịch, truy cập thuộc tính và thiết lập cache
// - Lời gọi thứ hai sử dụng lại cache, nhanh hơn nhiều
printX(a); // Thiết lập inline cache cho obj.x dựa trên hidden class của 'a'
printX(b); // Cache hit! Truy cập nhanh vì 'b' giống với hidden class của 'a' // Bây giờ tạo object với thứ tự thuộc tính khác
var c = { y: 3, x: 4 };
// 'c' có thuộc tính được thêm theo thứ tự khác, hidden class khác với 'a'/'b' // Gọi printX với 'c':
// - Engine thấy hidden class không trùng với inline cache.
// - Cache phải được cập nhật hoặc mở rộng (polymorphic inline cache).
printX(c); // Trượt cache hoặc dùng inline cache đa hình; có thể chậm hơn một chút // Ví dụ thêm thuộc tính động:
var d = {};
d.x = 5; // 'd' được cập nhật hidden class khi thêm thuộc tính printX(d); // Có thể trúng hoặc trượt cache, tùy thuộc vào thứ tự thêm thuộc tính và hidden class hiện tại // Nếu thay đổi shape sau khi object đã được tối ưu, engine có thể phải "deoptimize":
d.y = 10; // Thay đổi shape của 'd' (hidden class mới) printX(d); // Engine có thể quay về tra cứu thuộc tính chậm nếu hidden class thay đổi --- Nếu bạn cần thêm phần tiếp theo hoặc muốn tôi minh họa bằng hình ảnh, cứ nói nhé!
Cảm ơn các bạn đã theo dõi!