How Browsers Work · Part 16 — V8 Deep: Optimization Tiers & Deopt
Bên trong máy ảo JS: Ignition, Sparkplug, Maglev, TurboFan, hidden class, inline cache, elements kind và deoptimization — cùng cách đọc chúng mà không tối ưu mò.
Phần 6 cho ta bức tranh tổng thể V8: parse → bytecode → JIT. Phần này mở nắp động cơ. JavaScript là ngôn ngữ động — không khai báo kiểu — nhưng V8 chạy nó gần tốc độ máy bằng cách suy đoán kiểu rồi biên dịch tối ưu, và huỷ tối ưu khi suy đoán sai. Hiểu cơ chế này giải thích nhiều “mẹo hiệu năng” tưởng huyền bí.
1. Các tầng thực thi
V8 không biên dịch tối ưu mọi code ngay. Nó dùng kiến trúc tiered: bắt đầu với chi phí khởi động thấp, thu thập feedback, rồi chọn tier phù hợp cho code “nóng”. Sơ đồ dưới là đường đi phổ biến, không phải state machine mà mọi hàm bắt buộc đi qua đủ bốn tầng.
Source ─► Parse ─► Bytecode
│ feedback + tiering decisions
▼
Ignition (interpreter) ─► chạy bytecode, thu thập feedback
│ có thể tier-up / OSR khi đủ nóng
▼
Sparkplug (baseline JIT) ─► mã máy chưa tối ưu, biên dịch nhanh
│ nóng hơn
▼
Maglev (mid-tier JIT) ─► tối ưu vừa, biên dịch nhanh
│ rất nóng
▼
TurboFan (optimizing JIT) ─► hướng tới peak performance, compile đắt hơn
Mỗi tầng đánh đổi thời gian biên dịch lấy tốc độ chạy. Hàm chạy ít có thể ở Ignition/Sparkplug; code đủ nóng và tối ưu được có thể tới Maglev hoặc TurboFan, kể cả OSR (thay code khi vòng lặp đang chạy). Ngưỡng tiering và quyết định bỏ qua tier là chi tiết implementation thay đổi theo phiên bản/nền tảng.
Tên tier cũng che một pipeline đang tiến hoá: V8 cho biết toàn bộ JavaScript backend của top-tier TurboFan hiện dùng IR/compiler backend Turboshaft, còn các phần frontend tiếp tục được thay đổi. Với developer, “TurboFan” vẫn là nhãn top tier hữu ích; đừng coi sơ đồ thành danh sách component bất biến.
2. Hidden class (Shape / Map) — kiểu ẩn cho object
JS object là túi key→value động, nhưng nếu V8 lưu mỗi object như hash map thì sẽ chậm. Thay vào đó V8 gán mỗi object một hidden class (gọi nội bộ là Map/Shape) mô tả “hình dạng”: object có những thuộc tính nào, ở offset nào.
function P(x, y) { this.x = x; this.y = y; }
const a = new P(1, 2); // hidden class C0 → C1(x) → C2(x,y)
const b = new P(3, 4); // CÙNG chuỗi C0→C1→C2 ✅ chia sẻ shape
Object cùng “hình dạng” chia sẻ hidden class → V8 truy cập thuộc tính bằng offset cố định thay vì tra cứu. Nhưng thêm thuộc tính theo thứ tự khác nhau tạo hidden class khác nhau:
const a = {}; a.x = 1; a.y = 2; // shape {x,y}
const b = {}; b.y = 2; b.x = 1; // shape {y,x} — khác a
Trong code nóng, khởi tạo object cùng tập thuộc tính/cùng thứ tự giúp chia sẻ Map. Thêm/xoá nhiều property có thể làm V8 chọn dictionary properties, vốn ít cơ hội tối ưu hơn. Đây là gợi ý để kiểm chứng bằng benchmark/profile, không phải lý do bẻ API sạch thành code khó đọc.
3. Inline cache — mono, poly, megamorphic
Khi code đọc obj.x, V8 lưu feedback tại access site về những Map đã gặp — nền của inline cache (IC). Lần sau cùng shape, engine có thể đi qua fast path chuyên biệt.
Site đọc obj.x từng gặp:
1 shape → MONOMORPHIC (một fast path chuyên biệt)
một tập nhỏ → POLYMORPHIC (kiểm tra vài shape)
rất nhiều shape → MEGAMORPHIC (stub/cache tổng quát hơn)
Ngưỡng chính xác là chi tiết nội bộ và còn khác theo loại IC. Khi một access site gặp quá nhiều shape, V8 dùng đường megamorphic tổng quát hơn; nó không đơn giản “tắt cache”. Shape ổn định thường mở thêm cơ hội inline, nhưng chi phí thực tế phải được đo ở đúng workload.
4. Speculation & deopt — khi suy đoán sai
Maglev/TurboFan biên dịch dựa trên giả định từ feedback (ví dụ “x thường là Smi”). Nếu guard thất bại, V8 deoptimize: tái dựng frame/state tương ứng trong tier ít tối ưu hơn rồi tiếp tục với đúng semantics JavaScript. Mã tối ưu có thể được thay thế hoặc tối ưu lại sau đó, tuỳ feedback mới.
function add(a, b) { return a + b; }
for (let i = 0; i < 1e6; i++) add(i, i); // feedback chủ yếu là number
add('x', 'y'); // có thể phá specialization đã tạo; semantics vẫn là "xy"
Deopt thỉnh thoảng là cơ chế bình thường. Việc tối ưu/deopt lặp lại trong hot path mới đáng điều tra; nguyên nhân có thể là input đổi kiểu/shape, assumption về elements kind hoặc guard khác bị phá. V8 cũng có thể ngừng tối ưu một function thay vì lặp vô hạn, nên sơ đồ sau chỉ là failure mode khái niệm.
optimize → deopt → optimize → deopt ... ← lãng phí, chậm hơn cả không tối ưu
5. Elements kind — mảng cũng có “kiểu”
V8 chuyên hoá phần lưu trữ indexed elements. Bản đồ đơn giản hoá:
PACKED_SMI [1, 2, 3] (số nguyên nhỏ, liền; chuyên biệt)
PACKED_DOUBLE [1.5, 2.5] (double, liền)
PACKED_ELEMENTS [1, 'a', {}] (giá trị tổng quát, liền)
HOLEY_* [1, , 3] / a[100]=1 (có "lỗ" → cần thêm kiểm tra)
Elements kind thường chuyển theo hướng tổng quát hơn; từ 2025, V8 có ngoại lệ cho Array.prototype.fill, nên câu “không bao giờ đi ngược” không còn tuyệt đối. Quy tắc thực dụng vẫn là tránh vô tình tạo mảng thưa (arr[100] = x khi length là 5) trong hot path, và đừng tối ưu loại phần tử nếu profiler chưa cho thấy nó đáng kể.
6. Đọc tín hiệu tối ưu (khi cần)
Bạn hiếm khi cần, nhưng benchmark Node cô lập có thể bật cờ V8 để xem tiering/deopt:
node --trace-opt --trace-deopt benchmark.js
node --trace-deopt-verbose benchmark.js
Output và tên reason là chi tiết phiên bản. Tránh %OptimizeFunctionOnNextCall/--allow-natives-syntax trong code ứng dụng: intrinsics không phải API ổn định và có thể làm benchmark xa production. DevTools Performance vẫn giúp tìm function thật sự tốn CPU, nhưng các bản Chrome mới đã giảm/ẩn nhiều compile events; đừng suy ra deopt chỉ từ màu flame chart.
7. Tóm tắt
- V8 có các tier Ignition, Sparkplug, Maglev và TurboFan; đường đi/ngưỡng không cố định và OSR có thể thay code khi đang chạy.
- Hidden class (Map/Shape) và inline cache giúp chuyên hoá property access; dictionary/megamorphic path tổng quát hơn nhưng không đồng nghĩa “mất cache hoàn toàn”.
- Deopt tái dựng state đúng khi giả định speculative sai; nó bình thường trừ khi profile cho thấy churn trong hot path.
- Mảng có elements kind; packed thường dễ tối ưu hơn holey, nhưng lattice có ngoại lệ hiện đại như
Array.prototype.fill. - Tìm hotspot bằng profiler trước; dùng
--trace-opt/--trace-deopttrong benchmark cô lập khi thật cần, không xây production quanh V8 intrinsics.
Phần tiếp theo: Memory deep — heap snapshot/Comparison, retainer path, detached DOM, Oilpan GC và giới hạn của weak references cùng memory measurement API.