Mở đầu: chồng lấp thay vì ép mọi lệnh theo nhịp lệnh chậm nhất
Bài 3 kết thúc bằng một pitfall gai góc: datapath đơn chu kỳ ép TẤT CẢ
lệnh — kể cả một ADD đơn giản — chạy theo nhịp của lệnh CHẬM NHẤT (LW). Giải
pháp không phải "làm ADD chậm hơn cho công bằng" mà ngược lại: chia nhỏ mỗi lệnh thành 5 bước (giai đoạn)
và cho nhiều lệnh CHỒNG LẤP nhau — lệnh sau bắt đầu bước 1 ngay khi lệnh trước vừa xong bước 1. Đây là
pipeline, và cái giá của việc chồng lấp là: các lệnh đang "chạy song song" đôi khi cần DỮ
LIỆU của nhau — xung đột dữ liệu (data hazard).
1. Cơ chế Pipeline 5 giai đoạn
Mỗi lệnh RV32I đi qua đúng 5 bước: IF (Instruction Fetch — đọc lệnh), ID (Instruction Decode — giải mã + đọc thanh ghi), EX (Execute — tính toán qua ALU), MEM (Memory access — chỉ LW/SW thật sự dùng), WB (Write Back — ghi kết quả vào thanh ghi). Pipeline đặt MỘT khối phần cứng riêng cho mỗi giai đoạn, nối bằng các thanh ghi đệm liên giai đoạn (pipeline registers) — khi lệnh A đang ở EX, lệnh B (nạp sau A đúng 1 chu kỳ) đã có thể ở ID, và lệnh C đã có thể ở IF — cả 3 lệnh "sống" trong CPU CÙNG một lúc.
Chu ky: 1 2 3 4 5 6 7
Lenh A: IF ID EX MEM WB
Lenh B: IF ID EX MEM WB
Lenh C: IF ID EX MEM WB
# Tai chu ky 3: A dang o EX, B dang o ID, C dang o IF - CA 3 lenh
# "song" dong thoi trong CPU, moi lenh o MOT giai doan khac nhau.
2. Tính toán hiệu năng Pipeline
Thời gian chạy $N$ lệnh trên pipeline $S$ giai đoạn, xung nhịp $t_{clk}$, có $stallCycles$ chu kỳ "bong bóng" chèn thêm do hazard:
$$T = (N + S - 1 + stallCycles) \times t_{clk}$$
Số hạng $(S-1)$ là độ trễ "làm đầy" pipeline lúc khởi động — 4 chu kỳ đầu tiên (với $S=5$) chưa có đủ 5 lệnh chồng lấp cùng lúc. Verified bằng số thật: 1 triệu lệnh, pipeline 5 giai đoạn, xung nhịp 2GHz ($t_{clk}=0,5$ns) — KHÔNG có stall mất đúng 500.002 ns; CÙNG chương trình đó nhưng có 200.000 chu kỳ stall (vd do hazard) mất 600.002 ns — chênh lệch đúng bằng $200.000 \times 0,5\text{ns} = 100.000\text{ns}$.
function pipelineTime(numInstructions, numStages, stallCycles, clockPeriodNs) {
return (numInstructions + numStages - 1 + stallCycles) * clockPeriodNs;
}
function pipelineCPI(numInstructions, stallCycles) {
return (numInstructions + stallCycles) / numInstructions; // CPI ly tuong = 1
}
// Verified: pipelineTime(1_000_000, 5, 0, 0.5) === 500002 (ns)
// Verified: pipelineTime(1_000_000, 5, 200_000, 0.5) === 600002 (ns)
// Verified: pipelineCPI(1_000_000, 200_000) === 1.2 (CPI ly tuong la 1)
3. Xung đột dữ liệu (Data Hazards)
Khi lệnh sau CHỒNG LẤP với lệnh trước, nó có thể cần đọc/ghi một thanh ghi mà lệnh trước cũng đang đọc/ghi — 3 kiểu xung đột kinh điển:
| Kiểu | Ý nghĩa | Có xảy ra trong pipeline cơ bản? |
|---|---|---|
| RAW (Read-After-Write) | Lệnh sau ĐỌC thanh ghi mà lệnh trước chưa kịp GHI xong | Có — xung đột THẬT, cần forwarding/stall |
| WAR (Write-After-Read) | Lệnh sau GHI thanh ghi mà lệnh trước chưa kịp ĐỌC | Không — pipeline cơ bản đọc ở ID (sớm), ghi ở WB (muộn), thứ tự tự nhiên đã đúng |
| WAW (Write-After-Write) | 2 lệnh cùng GHI 1 thanh ghi sai thứ tự | Không — pipeline cơ bản hoàn thành WB đúng thứ tự chương trình (in-order) |
Forwarding (chuyển tiếp dữ liệu): thay vì CHỜ lệnh trước ghi xong vào thanh ghi rồi mới
đọc lại, kết quả ALU được "đi tắt" thẳng từ đầu ra EX của lệnh trước sang đầu vào EX của lệnh sau —
verified: chuỗi 3 lệnh RV32I thật ADDI x1,x0,20 / ADD x2,x1,x1 /
SUB x3,x2,x1 (2 hazard RAW liên tiếp) khi CÓ forwarding cho đúng 0 stall —
hoàn toàn không mất chu kỳ nào; khi TẮT forwarding, mỗi hazard cần đúng 2 stall (chờ tới khi WB xong),
tổng 4 stall.
function detectHazards(instrs, forwardingEnabled) {
let totalStalls = 0;
const hazards = [];
for (let i = 1; i < instrs.length; i++) {
const prev = instrs[i - 1], curr = instrs[i];
const prevWritesReg = /* prev la R/I/ILOAD va rd != 0 */;
if (!prevWritesReg) continue;
const reads = curr.rs1 === prev.rd || curr.rs2 === prev.rd;
if (!reads) continue;
if (prev.type === 'ILOAD') { totalStalls += 1; /* load-use: LUON 1 stall */ }
else if (!forwardingEnabled) { totalStalls += 2; /* RAW, khong forwarding */ }
// co forwarding + khong phai load-use -> 0 stall, khong lam gi them
}
return { totalStalls, hazards };
}
// Verified: chuoi RAW that CO forwarding -> 0 stall; KHONG forwarding -> 4 stall
LW chỉ có dữ liệu SẴN SÀNG ở giai đoạn
MEM (chậm hơn EX đúng 1 giai đoạn). Nếu lệnh NGAY SAU dùng thẳng kết quả đó, forwarding
vẫn không kịp — bắt buộc chèn đúng 1 chu kỳ stall, dù bật hay tắt forwarding. Verified
thật: LW x1, 0(x2) rồi ADD x3, x1, x1 (dùng ngay $x_1$) — bật forwarding vẫn
cho đúng 1 stall, không thể về 0 như hazard RAW thường.
4. Phân tích ví dụ thực tế & tính toán Stall
Giản đồ thời gian (pipeline clock grid) của ví dụ load-use — verified 1 stall đúng bằng cách "trễ" giai
đoạn EX của lệnh ADD lại đúng 1 chu kỳ để chờ dữ liệu từ MEM của LW:
| Lệnh | C1 | C2 | C3 | C4 | C5 | C6 | C7 |
|---|---|---|---|---|---|---|---|
LW x1,0(x2) |
IF | ID | EX | MEM | WB | ||
ADD x3,x1,x1 |
IF | ID | bubble | EX | MEM | WB |
Đúng công thức: 2 lệnh, 5 giai đoạn, 1 stall → $T = (2+5-1+1) = 7$ chu kỳ — khớp với cột cuối cùng (WB của ADD tại chu kỳ 7) trong bảng trên. CPI thực tế của đoạn mã 2 lệnh này: $CPI = (2+1)/2 = 1,5$ — cao hơn hẳn CPI lý tưởng $=1$ vì tỷ trọng stall trên tổng số lệnh RẤT lớn khi đoạn mã ngắn (đúng pitfall Mục 2 vừa nêu).
const seq = [assembleRV32I('LW', {rd:1, rs1:2, imm:0}), assembleRV32I('ADD', {rd:3, rs1:1, rs2:1})]
.map(decodeRV32I);
const { totalStalls } = detectHazards(seq, true); // forwarding bat, van 1 stall
const T = pipelineTime(2, 5, totalStalls, 0.5); // = 7 chu ky (don vi: t_clk)
const cpi = pipelineCPI(2, totalStalls); // = 1.5
// Verified: totalStalls=1, T=(2+5-1+1)=7, CPI=(2+1)/2=1.5
5. Thực hành: Mô phỏng Pipeline 5 giai đoạn RISC-V tương tác
Máy tính thời gian/CPI bên dưới dùng ĐÚNG công thức đã verify — đổi số lệnh/stall/xung nhịp để xem $T$ và CPI thay đổi trực tiếp. Bộ mô phỏng hazard cho phép bật/tắt forwarding trên 2 chuỗi lệnh RV32I thật (RAW và load-use) để tự tay thấy con số stall đổi đúng như Mục 3:
Máy tính T & CPI
Mô phỏng Hazard
Tóm lược
- ✅ Pipeline chồng lấp nhiều lệnh qua 5 giai đoạn (IF-ID-EX-MEM-WB) để tăng THÔNG LƯỢNG, không rút ngắn độ trễ 1 lệnh đơn lẻ.
- ✅ Verified: $T=(N+S-1+stall) \times t_{clk}$ — 1 triệu lệnh không stall = 500.002ns, có 200.000 stall = 600.002ns ở 2GHz.
- ✅ RAW là hazard THẬT duy nhất trong pipeline cơ bản (WAR/WAW tự động đúng thứ tự nhờ đọc sớm ở ID, ghi muộn ở WB, in-order commit).
- ✅ Verified: forwarding giải quyết RAW ALU-ALU hoàn toàn (0 stall); không forwarding cần 2 stall/hazard.
- ✅ Pitfall: load-use hazard (LW rồi dùng ngay) forwarding KHÔNG cứu được — verified luôn cần đúng 1 stall.
Trắc nghiệm ôn tập
Câu 1
Pipeline 5 giai đoạn giúp cải thiện điều gì so với datapath đơn chu kỳ?
Câu 2
Verified: 1 triệu lệnh không stall mất 500.002ns, có 200.000 stall mất 600.002ns (2GHz). Chênh lệch 100.000ns đến từ đâu?
Câu 3
Vì sao WAR và WAW KHÔNG xảy ra trong pipeline cơ bản (dù RAW vẫn xảy ra)?
Câu 4
Verified: LW x1,0(x2) rồi ADD x3,x1,x1 (dùng ngay x1) — forwarding vẫn cần đúng 1 stall. Vì sao forwarding không giải quyết được hoàn toàn như hazard RAW thường?
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 4
vừa thêm pipelineTime(), pipelineCPI(), detectHazards() — công
thức thời gian/CPI có stall và bộ phát hiện hazard RAW/load-use, 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 liệu tham khảo
- Sách giáo khoa cốt lõi: Computer Organization and Design, RISC-V Edition (Patterson & Hennessy) — Chương 4 trình bày chi tiết pipeline, hazard và forwarding.
- Khái niệm hazard: Wikipedia — Hazard (computer architecture) — tổng quan RAW/WAR/WAW và các kỹ thuật xử lý.
- Forwarding chi tiết: Wikipedia — Operand forwarding — cơ chế "bypass" dữ liệu giữa các giai đoạn pipeline.