Pipeline và hazard — CPU làm nhiều lệnh cùng lúc thế nào
Pipeline cho phép CPU chồng lấn nhiều lệnh như dây chuyền lắp ráp. Các loại hazard (data, control, structural) gây stall, và vì sao lệnh phụ thuộc nhau làm chậm code.
TL;DR: CPU không chờ một lệnh hoàn toàn xong mới bắt đầu lệnh tiếp theo. Thay vào đó, CPU chia mỗi lệnh thành nhiều pha (fetch, decode, execute, memory, writeback) và chạy các pha của nhiều lệnh song song — gọi là pipeline. Lý tưởng cho phép hoàn thành ~1 lệnh mỗi chu kỳ clock. Nhưng khi lệnh phụ thuộc kết quả của lệnh trước (data hazard), hoặc gặp nhánh rẽ chưa biết đi đâu (control hazard), pipeline phải dừng chờ (stall) — lãng phí chu kỳ. Hiểu điều này giúp bạn viết vòng lặp ít phụ thuộc dữ liệu hơn và thân thiện với branch prediction hơn.
Hãy thử nghĩ: nếu mỗi lệnh assembly mất 5 chu kỳ clock (fetch → decode → execute → memory → writeback), và CPU phải hoàn thành xong lệnh trước mới bắt đầu lệnh sau, thì để chạy 100 lệnh cần 500 chu kỳ. Bốn trong số 5 đơn vị phần cứng nghỉ suốt 80% thời gian. CPU đời đầu thật sự làm vậy — và chậm đúng vậy.
Bài này giải thích pipeline hoạt động ra sao, ba loại hazard phá vỡ hiệu quả pipeline, và hệ quả cụ thể lên code bạn viết hằng ngày.
1. Analogy — dây chuyền giặt là
Tưởng tượng bạn có ba mẻ quần áo cần xử lý, và ba công đoạn: giặt (30 phút), sấy (30 phút), gấp (30 phút). Cách ngây thơ: mẻ 1 giặt xong → sấy xong → gấp xong (90 phút), rồi mới bắt đầu mẻ 2. Tổng ba mẻ: 270 phút.
Cách thông minh hơn: trong khi mẻ 1 đang sấy, bỏ mẻ 2 vào máy giặt luôn. Khi mẻ 1 gấp thì mẻ 2 sấy và mẻ 3 giặt. Ba mẻ xong chỉ trong 150 phút — cùng một bộ máy móc, nhanh gấp 1.8 lần.
CPU pipeline là dây chuyền đó, áp dụng cho từng lệnh assembly.
| Dây chuyền giặt là | Pipeline CPU |
|---|---|
| Một mẻ quần áo | Một lệnh assembly |
| Công đoạn (giặt, sấy, gấp) | Pha pipeline (fetch, decode, execute, memory, writeback) |
| Chồng lấn nhiều mẻ | Chồng lấn nhiều lệnh ở các pha khác nhau |
| Thông lượng (throughput): số mẻ/giờ | Throughput CPU: số lệnh hoàn thành/chu kỳ |
| Khi máy giặt hỏng → dừng dây chuyền | Hazard → stall, pipeline đứng chờ |
Pipeline không làm một lệnh chạy nhanh hơn (latency không đổi), nhưng làm nhiều lệnh cùng tiến hành song song — throughput tăng. Giống mẻ giặt vẫn mất 90 phút, nhưng dây chuyền ra 1 mẻ/30 phút thay vì 1 mẻ/90 phút.
2. Cơ chế pipeline 5 pha
CPU hiện đại (x86, ARM) chia thực thi một lệnh thành 5 pha cổ điển:
| Pha | Tên đầy đủ | Làm gì |
|---|---|---|
| IF | Instruction Fetch | Đọc lệnh từ instruction cache theo program counter |
| ID | Instruction Decode | Giải mã opcode, đọc thanh ghi nguồn |
| EX | Execute | ALU tính toán (cộng, trừ, so sánh, shift...) |
| MEM | Memory Access | Đọc hoặc ghi RAM (chỉ lệnh load/store) |
| WB | Write Back | Ghi kết quả vào thanh ghi đích |
Mỗi pha mất đúng 1 chu kỳ clock. Không có pipeline: 1 lệnh = 5 chu kỳ, đơn vị khác chờ. Với pipeline 5 pha, sau 5 chu kỳ khởi động ban đầu, CPU hoàn thành 1 lệnh mỗi chu kỳ (lý tưởng).
Sơ đồ dưới minh hoạ 4 lệnh chồng lấn nhau — mỗi hàng là một lệnh, mỗi cột là một chu kỳ:
gantt title Pipeline 5 pha (ly tuong, khong co hazard) dateFormat X axisFormat Chu ky %s section Lenh 1 IF :done, l1if, 1, 2 ID :done, l1id, 2, 3 EX :done, l1ex, 3, 4 MEM :done, l1mem, 4, 5 WB :done, l1wb, 5, 6 section Lenh 2 IF :done, l2if, 2, 3 ID :done, l2id, 3, 4 EX :done, l2ex, 4, 5 MEM :done, l2mem, 5, 6 WB :done, l2wb, 6, 7 section Lenh 3 IF :done, l3if, 3, 4 ID :done, l3id, 4, 5 EX :done, l3ex, 5, 6 MEM :done, l3mem, 6, 7 WB :done, l3wb, 7, 8 section Lenh 4 IF :done, l4if, 4, 5 ID :done, l4id, 5, 6 EX :done, l4ex, 6, 7 MEM :done, l4mem, 7, 8 WB :done, l4wb, 8, 9
Chu ky: 1 2 3 4 5 6 7 8 9
Lenh 1: [IF] [ID] [EX] [MEM][WB]
Lenh 2: [IF] [ID] [EX] [MEM][WB]
Lenh 3: [IF] [ID] [EX] [MEM][WB]
Lenh 4: [IF] [ID] [EX] [MEM][WB]
Lenh 5: [IF] [ID] [EX] [MEM][WB]
Từ chu kỳ 5 trở đi, mỗi chu kỳ hoàn thành 1 lệnh. 100 lệnh mất khoảng 104 chu kỳ thay vì 500 — gần gấp 5 lần nhanh hơn.
Metric đo hiệu quả pipeline là IPC (Instructions Per Clock — số lệnh hoàn thành mỗi chu kỳ). Pipeline lý tưởng đạt IPC = 1. CPU superscalar hiện đại (bài 05) có thể vượt IPC = 4 nhờ nhiều pipeline chạy song song. Bài 01 giải thích clock và IPC nếu bạn cần ôn lại.
3. Hazard — vì sao pipeline không hoàn hảo
Ba loại tình huống phá vỡ nhịp pipeline, gọi chung là hazard:
3.1. Data hazard — lệnh chờ kết quả lệnh trước
Tình huống phổ biến nhất. Giả sử hai lệnh liên tiếp:
ADD R1, R2, R3 ; R1 = R2 + R3 (lenh A)
MUL R4, R1, R5 ; R4 = R1 * R5 (lenh B, can R1 tu lenh A)
Lệnh B cần giá trị R1 từ lệnh A. Nhưng lệnh A chỉ ghi R1 ở pha WB (chu kỳ 5), trong khi lệnh B cần đọc R1 ở pha ID (chu kỳ 3 của B). B đến sớm hơn A xong 2 chu kỳ.
Chu ky: 1 2 3 4 5 6 7 8 9
Lenh A: [IF] [ID] [EX] [MEM][WB]
Lenh B: [IF] [ID] ? ? ? ? <- can WB cua A
^^^^^ Doc R1 tai day -- R1 chua co gia tri!
Giải pháp 1 — stall (bubble): CPU chèn chu kỳ chờ rỗng ("bubble") vào pipeline cho đến khi lệnh A ghi xong:
Chu ky: 1 2 3 4 5 6 7 8
Lenh A: [IF] [ID] [EX] [MEM][WB]
Lenh B: [IF] [--] [--] [ID] [EX] [MEM][WB]
stall stall
Mỗi bubble là 1 chu kỳ lãng phí. Với dây chuyền phụ thuộc dài, stall cộng dồn đáng kể.
Giải pháp 2 — forwarding (bypassing): phần cứng kết nối trực tiếp đầu ra của pha EX tới đầu vào của pha EX lệnh sau, không chờ WB. Điều này giảm stall xuống còn 0–1 chu kỳ trong nhiều trường hợp. CPU hiện đại đều có forwarding; phần đào sâu bên dưới giải thích rõ hơn.
3.2. Control hazard — gặp nhánh rẽ, chưa biết đi đâu
Mỗi lệnh if, while, for trong code cấp cao đều biên dịch thành lệnh nhánh (branch) trong assembly. Khi CPU fetch lệnh nhánh, nó chưa biết điều kiện nhánh là đúng hay sai (vì lệnh nhánh chưa qua pha EX để tính điều kiện).
CPU có hai lựa chọn:
- Dừng fetch (stall) cho đến khi biết địa chỉ đích — phạt 2–3 chu kỳ mỗi nhánh.
- Đoán (speculate) — tiếp tục fetch theo một nhánh, nếu đoán sai thì flush (xoá) các lệnh đã fetch sai và fetch lại. Đây là branch prediction, chủ đề của bài 03.
Pipeline càng sâu, phạt khi đoán sai branch càng lớn. Pentium 4 với pipeline 20 pha, đoán sai nhánh tốn 20 chu kỳ flush — đây là lý do Pentium 4 kém hiệu quả hơn pipeline ngắn hơn của Pentium M dù clock cao hơn.
3.3. Structural hazard — tranh chấp tài nguyên phần cứng
Xảy ra khi hai lệnh cùng cần một đơn vị phần cứng cùng lúc. Ví dụ: CPU chỉ có một cổng đọc bộ nhớ, nhưng lệnh A ở pha MEM (đọc dữ liệu) và lệnh B ở pha IF (đọc lệnh) cùng muốn dùng. CPU hiện đại thường giải quyết bằng cách tách instruction cache và data cache (Harvard architecture variant), hoặc thêm nhiều cổng bộ nhớ. Structural hazard ít gặp hơn trong CPU desktop/server hiện đại nhưng vẫn xuất hiện trong vi điều khiển nhúng đơn giản.
4. Áp dụng vào code của bạn
Pipeline hazard không chỉ là lý thuyết kiến trúc — nó ảnh hưởng trực tiếp đến tốc độ vòng lặp nóng của bạn.
Chuỗi phụ thuộc dữ liệu dài là nguyên nhân phổ biến nhất gây stall:
// Tinh tong mang -- chuoi phu thuoc dai
long sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i]; // moi lenh ADD phu thuoc ket qua ADD truoc
}
Mỗi phép sum += arr[i] phụ thuộc kết quả của phép trước (phải chờ sum được ghi xong). Pipeline phải stall hoặc forwarding không giải quyết hết được, dẫn đến IPC thực tế thấp hơn nhiều so với lý tưởng.
Giải pháp: tách thành nhiều accumulator độc lập — compiler hoặc CPU có thể chạy chúng trên các đường pipeline khác nhau mà không có phụ thuộc giữa nhau:
// Nhieu accumulator -- giam phu thuoc, tang IPC thuc te
long sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0;
int i = 0;
for (; i + 3 < arr.length; i += 4) {
sum0 += arr[i]; // khong phu thuoc sum1, sum2, sum3
sum1 += arr[i + 1];
sum2 += arr[i + 2];
sum3 += arr[i + 3];
}
// Xu ly phan du
for (; i < arr.length; i++) sum0 += arr[i];
long total = sum0 + sum1 + sum2 + sum3;
Bốn biến sum0–sum3 không phụ thuộc nhau, cho phép pipeline xử lý song song không bị stall chéo. Trên x86 hiện đại, kỹ thuật này có thể tăng tốc 2–4 lần cho vòng lặp số học thuần tuý.
Kỹ thuật nhiều accumulator làm code khó đọc hơn. Compiler (GCC, Clang với -O2/-O3) thường tự vectorize và unroll vòng lặp đơn giản tốt hơn bạn viết tay. Chỉ làm tay khi profiling chỉ ra đúng vòng lặp này là bottleneck và compiler không tự tối ưu được (thường khi có aliasing hoặc điều kiện phức tạp).
Với branch hazard: tránh nhánh khó đoán trong vòng lặp nóng. Dữ liệu đã sắp xếp giúp branch prediction đoán đúng gần 100%; dữ liệu ngẫu nhiên gây miss liên tục. Bài 03 phân tích kỹ hơn với benchmark thực tế.
5. Đào sâu (tuỳ chọn)
Forwarding / bypassing chi tiết: Thay vì chờ WB ghi vào register file rồi ID mới đọc, phần cứng kéo dây trực tiếp từ đầu ra EX/MEM đến đầu vào EX của lệnh sau. Ví dụ: kết quả ADD ở EX chu kỳ 3 có thể forward sang EX của lệnh MUL ở chu kỳ 4 mà không cần stall. Forwarding không giải quyết được mọi hazard (load-use hazard: lệnh load phải qua MEM mới có dữ liệu, forwarding từ MEM đến EX vẫn cần 1 stall cycle), nhưng giảm đáng kể số bubble so với không forwarding.
Pipeline sâu và Pentium 4: Intel NetBurst (Pentium 4, 2000–2006) đẩy pipeline lên 20 pha (Prescott: 31 pha) để cho phép clock cao hơn (3.8 GHz). Nhưng pipeline sâu hơn đồng nghĩa branch misprediction penalty lớn hơn (tới 20–31 chu kỳ flush), và IPC thực tế thấp vì hazard nhiều hơn. Kết quả: Pentium M (Centrino) với pipeline 12 pha và clock 1.6 GHz thường nhanh hơn Pentium 4 3.0 GHz trong workload thực tế. Đây là bài học lịch sử rằng MHz/GHz không đồng nghĩa hiệu năng.
Superscalar: CPU hiện đại không dừng ở một pipeline — chúng có 4–8 pipeline song song (superscalar), mỗi chu kỳ có thể issue nhiều lệnh cùng lúc. Bài 05 giải thích out-of-order execution và superscalar. Các hazard trên đều áp dụng cho từng pipeline trong superscalar, nhưng phức tạp hơn vì các pipeline tương tác nhau.
6. Liên hệ các bài khác
- Bài 01 — Clock, IPC và vì sao GHz không phải tất cả: IPC phụ thuộc trực tiếp vào số stall pipeline. Pipeline đầy (ít hazard) → IPC cao. Hiểu bài này giúp bài 01 có ý nghĩa cụ thể.
- Bài 03 — Branch prediction: control hazard giới thiệu ở đây được giải quyết chi tiết tại đó — với benchmark đo branch miss rate thực tế và kỹ thuật viết code branch-friendly.
- Bài 05 — Superscalar và out-of-order: pipeline đơn (bài này) là nền tảng; superscalar là nhiều pipeline chạy song song, out-of-order là CPU tự sắp xếp lại lệnh để tránh hazard.
7. Tóm tắt
- Pipeline chia mỗi lệnh thành nhiều pha (IF, ID, EX, MEM, WB) và chồng lấn nhiều lệnh, cho phép hoàn thành ~1 lệnh/chu kỳ — throughput tăng, latency một lệnh không đổi.
- Pipeline không làm lệnh nhanh hơn mà làm nhiều lệnh tiến hành song song.
- Data hazard: lệnh phụ thuộc kết quả lệnh trước chưa xong → stall hoặc forwarding. Chuỗi phụ thuộc dài làm giảm IPC thực tế.
- Control hazard: lệnh nhánh chưa biết địa chỉ đích → CPU đoán (branch prediction) hoặc stall. Đoán sai → flush pipeline, tốn nhiều chu kỳ.
- Structural hazard: tranh chấp tài nguyên phần cứng → stall; CPU hiện đại hầu như giải quyết bằng thiết kế (tách cache, nhiều cổng).
- Pipeline sâu hơn cho phép clock cao hơn nhưng phạt misprediction lớn hơn — Pentium 4 là ví dụ về tradeoff này.
- Viết code ít phụ thuộc dữ liệu (nhiều accumulator độc lập) và branch-friendly (dữ liệu sắp xếp) giúp pipeline hoạt động hiệu quả hơn — nhưng đo trước khi tối ưu.
8. Tự kiểm tra
Q1Pipeline cải thiện latency (thời gian chạy một lệnh) hay throughput (số lệnh hoàn thành mỗi đơn vị thời gian)? Giải thích vì sao.▸
Q2Data hazard xảy ra khi nào? Hai cách CPU giải quyết data hazard là gì?▸
Q3Vì sao Pentium 4 với pipeline 20 pha và clock 3.8 GHz lại thua Pentium M pipeline 12 pha clock 1.6 GHz trong nhiều workload thực tế?▸
Q4Đoạn code tính tổng mảng với biến sum đơn lẻ bị ảnh hưởng bởi loại hazard nào? Kỹ thuật nhiều accumulator giải quyết điều đó thế nào?▸
sum += arr[i] bị ảnh hưởng bởi data hazard: mỗi lần lặp phụ thuộc kết quả của lần lặp trước (ADD phải xong, ghi sum xong, lần sau mới đọc được). Đây là chuỗi phụ thuộc dài, pipeline phải stall hoặc forwarding nhưng vẫn không tránh khỏi độ trễ. Kỹ thuật nhiều accumulator (sum0, sum1, sum2, sum3) tách thành bốn chuỗi phụ thuộc độc lập nhau — lệnh ADD cho sum0 không phụ thuộc ADD cho sum1, pipeline xử lý song song không bị chặn, IPC tăng lên gần lý tưởng.Q5Trong ba loại hazard (data, control, structural), loại nào gây ra bởi lệnh rẽ nhánh (branch)? CPU hiện đại xử lý thế nào?▸
Q6Forwarding (bypassing) giải quyết được mọi data hazard không? Trường hợp nào vẫn cần stall dù có forwarding?▸
Q7Tại sao structural hazard ít gặp hơn trong CPU desktop/server hiện đại so với vi điều khiển đơn giản?▸
Bài tiếp theo: Branch prediction
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên