Mở đầu: một nhân chạy nhanh hơn bằng cách phát nhiều lệnh cùng lúc

Bài 5 giữ pipeline luôn ĐẦY lệnh bằng cách đoán trước hướng rẽ nhánh. Nhưng pipeline vô hướng (scalar) của Bài 4 vẫn chỉ PHÁT đúng 1 lệnh/chu kỳ, và luôn theo ĐÚNG thứ tự chương trình. CPU hiệu năng cao hiện đại đi xa hơn: phát nhiều lệnh mỗi chu kỳ (superscalar), và cho phép lệnh SAU chạy xong TRƯỚC lệnh trước nếu không có phụ thuộc dữ liệu THẬT — gọi là thực thi ngoài thứ tự (Out-of-Order execution, OOO). Cái giá: phải giải quyết phụ thuộc dữ liệu GIẢ sinh ra do số thanh ghi kiến trúc hữu hạn, và phải đảm bảo trạng thái kiến trúc cuối cùng vẫn ĐÚNG như chạy tuần tự — bài này xây thuật toán kinh điển giải quyết cả hai: Tomasulo.


📚 Điều kiện tiên quyết
Bắt buộc đọc Bài 4 (pipeline, hazard RAW) và Bài 5 (dự đoán nhánh). OOO xây THẲNG trên 2 bài này, KHÔNG cần kiến thức về cache hay bộ nhớ ảo (sẽ học ở Bài 7-8).

1. Song song cấp lệnh (ILP) & CPU Superscalar

ILP (Instruction-Level Parallelism) đo mức độ các lệnh LIỀN KỀ trong chương trình có thể chạy song song mà không đổi kết quả. CPU vô hướng (scalar) phát 1 lệnh/chu kỳ; CPU siêu vô hướng (superscalar) có nhiều đơn vị chức năng (ALU, bộ nhân, bộ tải/lưu) và có thể phát/thực thi NHIỀU lệnh đồng thời — nhưng tận dụng được bao nhiêu phụ thuộc HOÀN TOÀN vào việc chương trình có đủ lệnh ĐỘC LẬP hay không.

⚠️ Cạm bẫy: superscalar không tự động tăng tốc mọi chương trình
Hiệu năng thực tế của một CPU superscalar bị NGHẼN bởi các phụ thuộc dữ liệu THẬT (RAW — lệnh sau cần kết quả của lệnh trước) — dù có 8 đơn vị chức năng, một chuỗi lệnh phụ thuộc dây chuyền (mỗi lệnh cần kết quả lệnh ngay trước) vẫn phải chạy gần như TUẦN TỰ, không thể song song hoá dù phần cứng cho phép. Verified ở Mục 4: cùng 3 lệnh, chuỗi RAW thật (mỗi lệnh phụ thuộc lệnh trước) mất 11 chu kỳ, trong khi phiên bản có phụ thuộc GIẢ (WAR/WAW, loại bỏ được) chỉ mất 9 chu kỳ.

2. Phụ thuộc dữ liệu giả & Đổi tên thanh ghi (Register Renaming)

Ngoài RAW (hazard THẬT, đã học ở Bài 4), tồn tại 2 loại phụ thuộc chỉ xuất hiện vì CHƯƠNG TRÌNH tái sử dụng cùng MỘT tên thanh ghi kiến trúc cho 2 giá trị KHÔNG liên quan nhau về mặt logic:

Kiểu Ý nghĩa Có phải phụ thuộc THẬT?
RAW (Read-After-Write) Lệnh sau ĐỌC giá trị lệnh trước GHI ra Có — phụ thuộc dữ liệu thật, KHÔNG thể loại bỏ
WAR (Write-After-Read) Lệnh sau GHI đè thanh ghi mà lệnh TRƯỚC đang đọc giá trị CŨ Không — phụ thuộc GIẢ, chỉ vì trùng TÊN thanh ghi
WAW (Write-After-Write) 2 lệnh cùng GHI một thanh ghi, phải giữ đúng thứ tự "ai ghi sau thắng" Không — phụ thuộc GIẢ, chỉ vì trùng TÊN thanh ghi

Đổi tên thanh ghi (register renaming) loại bỏ hoàn toàn WAR/WAW: mỗi lần một lệnh GHI vào thanh ghi kiến trúc, nó được gán một "phiên bản" MỚI (trong Tomasulo là chỉ số ROB — Reorder Buffer), và Register Alias Table (RAT) ghi nhớ "thanh ghi X hiện đang trỏ tới phiên bản nào". Lệnh sau đọc/ghi thanh ghi đó sẽ tự động dùng ĐÚNG phiên bản tương ứng — không còn tranh chấp vật lý nào giữa các "phiên bản" khác nhau của cùng một tên thanh ghi.

register_renaming_concept.txt (RAT — Register Alias Table)
// RAT[r] = null          -> gia tri DUNG cua thanh ghi r nam trong regFile
// RAT[r] = robIndex       -> gia tri MOI nhat cua r con dang "bay" trong ROB
//                            tai vi tri robIndex, CHUA duoc commit

// Khi ISSUE mot lenh ghi vao thanh ghi dest:
//   RAT[dest] = robIndexMoiVuaCapPhat   // "DOI TEN" - tao PHIEN BAN MOI
// -> lenh SAU do doc "dest" se lay dung PHIEN BAN MOI nay qua RAT,
//    con lenh TRUOC do (da doc "dest" luc issue, truoc khi bi doi ten)
//    van giu nguyen gia tri CU cua no - khong bi anh huong boi lenh ghi de.
⚠️ Cạm bẫy: nhầm RAW (không loại bỏ được) với WAR/WAW (loại bỏ được)
RAW là ràng buộc DỮ LIỆU thật — nếu lệnh B cần kết quả TÍNH TOÁN của lệnh A, không có mẹo đổi tên nào giúp B chạy được TRƯỚC KHI A tính xong (đó là vi phạm luận lý chương trình, không phải giới hạn phần cứng). Ngược lại, WAR/WAW chỉ là ràng buộc về TÊN — đổi tên thanh ghi giải quyết HOÀN TOÀN, không đổi thứ tự tính toán chút nào. Verified: instr2 (WAR trên R2 với instr1 MUL) bắt đầu thực thi ở chu kỳ 3, RẤT LÂU trước khi instr1 hoàn tất ở chu kỳ 6 — chứng minh renaming loại bỏ stall hoàn toàn.

3. Thuật toán Tomasulo: Reservation Station + CDB + ROB

Tomasulo (IBM 360/91, 1967) là thuật toán OOO đầu tiên và vẫn là nền tảng của CPU OOO hiện đại (x86, ARM). 3 thành phần chính:

  • Reservation Station (RS) — mỗi đơn vị chức năng có vài "trạm đặt chỗ" giữ lệnh đang chờ toán hạng; khi CẢ 2 toán hạng sẵn sàng, RS tự động bắt đầu thực thi (không cần đợi lệnh trước xong nếu không phụ thuộc).
  • CDB (Common Data Bus) — bus dữ liệu DÙNG CHUNG để broadcast kết quả: mọi RS đang chờ giá trị đó "nghe lỏm" (snoop) CDB và tự cập nhật toán hạng ngay khi giá trị xuất hiện. Vì là tài nguyên DÙNG CHUNG, CHỈ 1 lệnh được ghi kết quả (write-result) mỗi chu kỳ — lệnh THUA phải đợi thêm.
  • ROB (Reorder Buffer) — bộ đệm giữ MỌI lệnh đang "bay" theo ĐÚNG thứ tự chương trình; dù các lệnh hoàn tất TÍNH TOÁN ngoài thứ tự, commit (ghi chính thức vào thanh ghi kiến trúc) luôn lấy từ ĐẦU ROB — đảm bảo trạng thái kiến trúc cuối cùng giống hệt chạy tuần tự.
tomasulo_cycle.js (trích engine dùng chung cpu-core.js)
// Moi chu ky, DUNG THU TU: Commit -> Write-result (1 broadcast/chu ky)
// -> giam remaining cac RS dang thuc thi -> WAITING->EXECUTING khi du toan
// hang -> Issue 1 lenh moi (doi ten dest qua RAT).
function runTomasulo(instructions, opts) {
  // ... xem cpu-core.js day du - day la vong lap chinh moi chu ky ...
  // 1. COMMIT dau ROB neu ready
  // 2. WRITE-RESULT: chi 1 broadcast/chu ky, uu tien robIndex NHO HON (lenh cu hon)
  // 3. Giam remaining RS dang EXECUTING
  // 4. WAITING -> EXECUTING khi Qj/Qk deu null (da nhan du toan hang qua CDB)
  // 5. ISSUE 1 lenh: RAT[dest] = robIndex moi (DOI TEN)
}
// Verified: chuong trinh MUL R1/ADD R2/SUB R1 (WAR tren R2, WAW tren R1)
// -> totalCycles=9, ipc=3/9=0.333, R1 cuoi=18 (tu SUB, dung in-order commit)
⚠️ Cạm bẫy: ROB đầy gây "ROB Stall"
ROB có DUNG LƯỢNG HỮU HẠN — nếu một lệnh bị kẹt lâu (vd chờ dữ liệu từ bộ nhớ chậm — sẽ học ở Bài 7), nó chiếm giữ đúng vị trí ĐẦU ROB (vì commit phải đúng thứ tự) khiến MỌI lệnh SAU nó, dù đã tính toán xong từ lâu, cũng không thể commit và phải "xếp hàng" trong ROB. Khi ROB đầy, CPU buộc phải NGỪNG phát lệnh mới hoàn toàn (ROB stall) — dù có hàng chục đơn vị chức năng đang rảnh rỗi. Đây là lý do CPU hiện đại đầu tư ROB rất lớn (hàng trăm entry) nhưng vẫn không tránh khỏi stall khi gặp lệnh trễ cực lâu (vd cache miss xuống tận DRAM).

4. Tính toán IPC thực tế & hiệu quả thuật toán

Verified thật bằng engine: chương trình 3 lệnh MUL R1,R2,R3 / ADD R2,R4,R5 (WAR trên R2) / SUB R1,R6,R7 (WAW trên R1) — đổi tên giải quyết cả 2 phụ thuộc giả, chạy đúng 9 chu kỳ, IPC = $3/9 = \mathbf{0,333}$. R1 cuối cùng = 18 (từ SUB, đúng thứ tự chương trình nhờ in-order commit — KHÔNG phải 12 từ MUL bị ghi đè, đúng ngữ nghĩa WAW "ai ghi sau thắng"). Đáng chú ý: ADD (instr2) hoàn tất TÍNH TOÁN (writeback chu kỳ 5) TRƯỚC MUL (instr1, writeback chu kỳ 6) — hoàn thành NGOÀI thứ tự — nhưng vẫn commit ĐÚNG thứ tự chương trình (chu kỳ 7, 8, 9 cho instr1, instr2, instr3).

tomasulo_schedule_table.txt (bảng lập lịch THẬT, verified bằng engine)
Lenh                Issue  ExecStart  Writeback  Commit
MUL R1, R2, R3        1        2          6          7
ADD R2, R4, R5        2        3          5          8
SUB R1, R6, R7        3        4          7          9

# ADD (writeback=5) xong TRUOC MUL (writeback=6) - hoan thanh NGOAI thu tu.
# Nhung commit van DUNG thu tu chuong trinh: 7, 8, 9 (MUL, ADD, SUB).
# Tong 9 chu ky, IPC = 3/9 = 0.333.

Đối chứng: CÙNG 3 phép tính đó nhưng viết thành chuỗi RAW THẬT (mỗi lệnh dùng kết quả lệnh ngay trước) — mất tới 11 chu kỳ, IPC chỉ $3/11 = \mathbf{0,273}$ — chậm hơn hẳn vì phụ thuộc thật KHÔNG có cách nào loại bỏ, đúng pitfall Mục 1.

ipc_comparison.js (đối chứng WAR/WAW vs RAW thật, dùng đúng engine)
// Chuong trinh A: MUL R1 / ADD R2 (WAR) / SUB R1 (WAW) -> 9 chu ky, IPC=0.333
// Chuong trinh B: MUL R1 / ADD R2,R1 (RAW that) / SUB R3,R2 (RAW that)
//                 -> 11 chu ky, IPC=0.273 (cham hon vi PHU THUOC THAT)
// Chuong trinh C: MUL R1 / ADD R4 / SUB R5 (khong xung dot gi ca)
//                 -> CUNG 9 chu ky nhu chuong trinh A - chung minh renaming
//                    da lam A nhanh NGANG BANG truong hop khong co xung dot!
⚠️ Cái giá của OOO: diện tích silicon & năng lượng
Logic điều khiển OOO (bảng RAT, nhiều Reservation Station, ROB, mạng CDB broadcast tới MỌI RS mỗi chu kỳ) cực kỳ phức tạp so với pipeline vô hướng đơn giản — chiếm diện tích silicon và tiêu thụ năng lượng đáng kể, không tỷ lệ thuận với mức tăng IPC thu được (lợi ích giảm dần khi ILP sẵn có trong chương trình đã cạn — xem lại pitfall Mục 1). Đây chính là lý do các lõi tiết kiệm điện (vd nhân "LITTLE" — sẽ gặp lại ở Bài 9) thường dùng pipeline vô hướng đơn giản hơn thay vì OOO đầy đủ.

5. Thực hành: Bộ giả lập Tomasulo thu nhỏ tương tác

Chọn một chương trình mẫu bên dưới để xem bảng lập lịch Tomasulo THẬT (Issue/Exec Start/Writeback/Commit từng lệnh) — so sánh trực tiếp chương trình có WAR/WAW (renaming giải quyết) với chuỗi RAW thật (không giải quyết được) và chương trình không xung đột gì:

🗂️ Bộ giả lập Tomasulo (RS + CDB + ROB)

Tóm lược

  • ✅ CPU superscalar phát nhiều lệnh/chu kỳ, nhưng bị nghẽn bởi phụ thuộc dữ liệu THẬT (RAW) — verified: chuỗi RAW thật mất 11 chu kỳ so với 9 chu kỳ của phiên bản có WAR/WAW.
  • ✅ WAR/WAW là phụ thuộc GIẢ (chỉ vì trùng tên thanh ghi) — đổi tên thanh ghi (RAT) loại bỏ HOÀN TOÀN, khác hẳn RAW không thể loại bỏ.
  • ✅ Tomasulo = Reservation Station (chờ toán hạng) + CDB (broadcast, 1 lệnh/chu kỳ) + ROB (commit đúng thứ tự chương trình dù hoàn thành ngoài thứ tự).
  • ✅ Verified: R1 cuối cùng = 18 (từ SUB, lệnh ghi SAU trong chương trình) — in-order commit đảm bảo đúng ngữ nghĩa WAW dù ADD hoàn tất tính toán trước MUL.
  • ✅ Pitfall: ROB đầy (thường do một lệnh kẹt lâu ở đầu ROB) gây ROB stall, ngừng phát lệnh mới hoàn toàn.

Trắc nghiệm ôn tập

Câu 1

Vì sao CPU superscalar không tự động tăng tốc MỌI chương trình lên gấp nhiều lần?

Câu 2

Đổi tên thanh ghi (register renaming) giải quyết được loại phụ thuộc nào, và KHÔNG giải quyết được loại nào?

Câu 3

Verified: trong chương trình WAR/WAW, ADD (instr2) writeback ở chu kỳ 5, TRƯỚC MUL (instr1, chu kỳ 6) — nhưng ADD lại commit ở chu kỳ 8, SAU MUL (chu kỳ 7). Vì sao?

Câu 4

"ROB Stall" xảy ra khi nào, và vì sao nó dừng CPU dù nhiều đơn vị chức năng vẫn rảnh?

Tải file code thực hành minh họa bài học

File JavaScript CPUJS — thư viện kiến trúc máy tính mini dùng xuyên suốt cả 12 bài, Bài 6 vừa thêm runTomasulo() — mô phỏng chu kỳ-chính-xác thuật toán Tomasulo (Reservation Station, CDB, ROB, đổi tên thanh ghi), kèm self-test đối chiếu đúng mọi con số trong bài (chạy node cpu-core.js, không cần cài thêm gì):

Tải về cpu-core.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 5: Dự Đoán Nhánh & Lỗ Hổng Bảo Mật Spectre Bài 7: Phân Cấp Bộ Nhớ & Kiến Trúc Cache Quay lại Lộ trình Kiến Trúc Máy Tính