Dữ liệu & CPU/Chu kỳ fetch-decode-execute — một lệnh đi qua CPU thế nào
10/23
Bài 10 / 23~14 phútMáy chạy thế nàoMiễn phí lượt xem

Chu kỳ fetch-decode-execute — một lệnh đi qua CPU thế nào

Vòng lặp cốt lõi mọi CPU lặp hàng tỷ lần mỗi giây: nạp lệnh, giải mã, thực thi. Vai trò program counter, instruction register và nhịp clock điều phối ra sao.

TL;DR: Mọi CPU đều chạy một vòng lặp vô tận gồm ba pha: Fetch (nạp lệnh từ bộ nhớ vào chip), Decode (control unit giải mã lệnh đó là làm gì), và Execute (ALU tính toán hoặc đọc/ghi bộ nhớ). Program counter (PC) theo dõi lệnh tiếp theo, instruction register (IR) giữ lệnh hiện tại, và nhịp clock đẩy từng pha tiến lên. Hiểu ba pha này, bạn hiểu được tại sao "nhiều dòng code" không đồng nghĩa "chạy chậm hơn" và tại sao truy cập bộ nhớ đắt hơn phép tính thuần túy.

Bạn nghe nói CPU chạy 3 GHz — tức 3 tỷ nhịp clock mỗi giây. Nhưng điều đó thực sự có nghĩa là gì? CPU đang làm gì trong 3 tỷ nhịp đó? Câu trả lời là nó đang lặp đi lặp lại một vòng lặp cực kỳ đơn giản: nạp lệnh → giải mã → thực thi → nạp lệnh tiếp → giải mã → thực thi → ... mãi cho đến khi máy tắt.

Bài này giải thích vòng lặp đó — từng pha là gì, ai điều phối, và tại sao hiểu nó giúp bạn lập trình tốt hơn.

1. Analogy — công nhân dây chuyền đơn

Hãy hình dung một công nhân làm việc ở dây chuyền sản xuất. Quy trình của anh ta lặp đi lặp lại suốt ngày:

  1. Lấy đơn hàng từ kệ phiếu — anh ta cầm tờ phiếu tiếp theo trong xếp hàng.
  2. Đọc hiểu yêu cầu trên phiếu — tờ phiếu ghi "lắp ốc vít số 4 vào vị trí B3", anh ta hiểu phải dùng dụng cụ nào, lấy linh kiện nào.
  3. Thực hiện công việc — vặn ốc vào đúng chỗ, dùng đúng dụng cụ.
  4. Lấy đơn hàng tiếp theo. Lặp lại.

CPU làm đúng điều này, chỉ khác là với lệnh máy thay vì phiếu giấy, và tốc độ hàng tỷ lần mỗi giây.

Dây chuyền sản xuấtCPU
Kệ phiếu đơn hàngBộ nhớ RAM (chứa các lệnh chương trình)
Số thứ tự phiếu hiện tạiProgram counter (PC)
Phiếu đang cầm trên tayInstruction register (IR)
Đọc hiểu yêu cầu trên phiếuPha Decode (control unit giải mã)
Thực hiện công việc tayPha Execute (ALU hoặc đọc/ghi bộ nhớ)
Giám sát viên đếm nhịpClock
💡 Cách nhớ

Fetch = lấy phiếu. Decode = đọc phiếu. Execute = làm theo phiếu. PC là con trỏ "đang đọc phiếu số mấy".

2. Ba pha: Fetch → Decode → Execute

2.1 Pha Fetch — nạp lệnh

CPU cần biết lệnh tiếp theo là gì. Để làm vậy, nó dùng program counter (PC) — một thanh ghi đặc biệt lưu địa chỉ bộ nhớ của lệnh sắp thực thi.

Trong pha Fetch:

  1. CPU đọc nội dung ở địa chỉ bộ nhớ mà PC đang trỏ tới.
  2. Bytes đọc được chuyển vào instruction register (IR) — thanh ghi tạm giữ lệnh đang xử lý.
  3. PC tăng lên để trỏ sang lệnh tiếp theo (trên kiến trúc 32-bit như ARM tăng 4 byte; trong ví dụ 8-bit đơn giản ở trên, tăng 1-2 byte tuỳ độ dài lệnh).
Truoc Fetch:   PC = 0x1000
               RAM[0x1000] = 10110001  (mot lenh ma hoa)

Sau Fetch:     IR  = 10110001  (lenh da vao chip)
               PC  = 0x1004   (tang len, chuan bi lenh ke)

2.2 Pha Decode — giải mã

Control unit — bộ não điều phối của CPU — đọc nội dung IR và giải mã xem lệnh yêu cầu làm gì.

Mỗi lệnh máy có cấu trúc nhị phân định sẵn: một phần là opcode (mã thao tác — ví dụ "cộng", "load từ RAM", "nhảy tới địa chỉ X"), phần còn lại là toán hạng (operand — ví dụ "thanh ghi nào", "địa chỉ nào", "hằng số bao nhiêu").

Ví dụ trên kiến trúc đơn giản 8-bit:

Lenh nhi phan: 1011 0001
               ^^^^       <- opcode: "LOAD thanh ghi A tu bo nho"
                   ^^^^   <- operand: dia chi 0x01

Control unit doc: "phai load gia tri tai dia chi 0x01 vao thanh ghi A"

2.3 Pha Execute — thực thi

Dựa trên kết quả decode, control unit ra lệnh cho phần cứng thích hợp:

  • ALU (Arithmetic Logic Unit) thực hiện phép tính (cộng, trừ, AND, OR, dịch bit...).
  • Bus bộ nhớ mang dữ liệu từ RAM vào thanh ghi (LOAD) hoặc ngược lại (STORE).
  • PC bị ghi đè nếu lệnh là nhảy/rẽ nhánh (JUMP, BRANCH) — đây là cơ chế điều khiển luồng.

Sau khi Execute xong, CPU quay lại Fetch với PC mới, vòng lặp tiếp tục.

3. Sơ đồ vòng lặp

flowchart LR
  F["FETCH\n(doc lenh tu RAM vao IR,\ntang PC)"]
  D["DECODE\n(control unit giai ma\nopcode + toan hang)"]
  E["EXECUTE\n(ALU tinh toan /\ndoc-ghi bo nho /\nghi de PC neu jump)"]

  F --> D --> E --> F

Nhịp clock đẩy từng pha tiến lên: một lệnh đơn giản có thể mất 1–4 chu kỳ clock; CPU hiện đại pipeline hóa để chồng lấn nhiều lệnh cùng lúc (xem Tầng đào sâu dưới).

4. Trace một lệnh thực tế

Giả sử chương trình pseudo-assembly sau:

; Chuong trinh don gian
; Khoi dau: R0 = 5, R1 = 3
LOAD  R2, [0x200]   ; lenh tai dia chi 0x100: doc RAM[0x200] vao R2
ADD   R3, R0, R1    ; lenh tai dia chi 0x104: R3 = R0 + R1

Trace lệnh LOAD R2, [0x200] (PC = 0x100):

PhaĐiều gì xảy ra
FetchPC = 0x100 → đọc RAM[0x100] → nội dung vào IR; PC tăng lên 0x104
DecodeControl unit đọc IR: opcode = LOAD, toán hạng = thanh ghi R2, địa chỉ nguồn = 0x200
ExecuteBus bộ nhớ đọc RAM[0x200]; giá trị đọc được ghi vào R2; ALU không tham gia

Trace lệnh ADD R3, R0, R1 (PC = 0x104):

PhaĐiều gì xảy ra
FetchPC = 0x104 → đọc RAM[0x104] → nội dung vào IR; PC tăng lên 0x108
DecodeControl unit đọc IR: opcode = ADD, nguồn 1 = R0, nguồn 2 = R1, đích = R3
ExecuteALU cộng giá trị R0 và R1; kết quả ghi vào R3; không đụng RAM

5. Lệnh nhảy và điều khiển luồng

Khi CPU gặp lệnh JUMP (hoặc BRANCH — nhảy có điều kiện):

JUMP 0x300    ; nhay toi dia chi 0x300

Trong pha Execute, thay vì ALU tính toán, control unit ghi đè PC bằng địa chỉ đích (0x300). Lần Fetch tiếp theo sẽ nạp lệnh tại 0x300, không phải lệnh kế tiếp trong dãy.

Đây là cơ chế nền tảng của mọi if, while, for, gọi hàm — tất cả đều dịch ra lệnh nhảy ở tầng máy. Khi CPU không biết trước điều kiện nhảy (true hay false), nó phải đoán — gọi là branch prediction. Đoán sai phải huỷ và làm lại — chi phí đáng kể ở CPU pipeline. Bài sau trong module sẽ đề cập cụ thể hơn.

📚 Đào sâu (tuỳ chọn) — một lệnh tốn bao nhiêu clock?

Mô tả ba pha ở trên là mô hình đơn chu kỳ (single-cycle) — đơn giản để học, nhưng CPU thật phức tạp hơn nhiều:

Multi-cycle: Phần lớn lệnh thực ra tốn nhiều chu kỳ clock. LOAD từ RAM có thể tốn 10–100+ chu kỳ (do RAM chậm hơn CPU nhiều bậc). Lệnh ADD đơn thuần có thể chỉ tốn 1 chu kỳ.

Pipeline: CPU hiện đại không chờ lệnh 1 xong mới bắt đầu lệnh 2. Trong khi lệnh 1 đang Execute, lệnh 2 đã đang Decode, lệnh 3 đã đang Fetch — chồng lấn như dây chuyền. Module 3 sẽ đào sâu cơ chế này.

CISC vs RISC: Kiến trúc CISC (x86) cho phép một lệnh làm nhiều việc phức tạp (ví dụ MOVS sao chép vùng nhớ). Kiến trúc RISC (ARM, RISC-V) thiết kế lệnh đơn giản, đồng đều — dễ pipeline hơn, từng lệnh chạy nhanh hơn nhưng cần nhiều lệnh hơn để làm cùng việc. x86 bên trong thực ra dịch lệnh phức tạp thành micro-ops RISC-like trước khi chạy.

Clock cycle vs instruction: Tốc độ CPU 3 GHz nghĩa là 3 tỷ chu kỳ clock mỗi giây, không phải 3 tỷ lệnh. Một lệnh thường tốn nhiều chu kỳ; đôi khi CPU pipeline cho phép hoàn thành trên 1 lệnh/chu kỳ trung bình nhờ cơ chế superscalar.

6. Áp dụng vào code của bạn

Hiểu chu kỳ fetch-decode-execute thay đổi cách bạn lý luận về hiệu năng:

Đếm dòng code không đo được tốc độ. Một dòng a = b + c dịch ra 1–3 lệnh máy và tốn vài chu kỳ clock. Một dòng result = database.query(...) dịch ra hàng trăm lệnh máy, hàng nghìn chu kỳ chờ I/O. Tốc độ nằm ở số chu kỳ clock thực tế, không phải số dòng.

Truy cập bộ nhớ đắt hơn phép tính thuần túy. Trong pha Execute, lệnh ADD đụng ALU — hoàn thành trong 1 chu kỳ. Lệnh LOAD đụng RAM — có thể tốn hàng trăm chu kỳ chờ dữ liệu truyền qua bus. Đây là lý do cache tồn tại (bài tiếp trong module sẽ đề cập).

Vòng lặp nóng: giảm công việc trong Execute. Nếu một vòng lặp chạy hàng triệu lần, mỗi lần bớt được một lần đọc RAM là bớt được hàng triệu chu kỳ clock. Tư duy: dữ liệu nào có thể giữ trong thanh ghi (hoặc cache L1) thay vì đọc từ RAM mỗi vòng?

# Vi du tu duy: doc bien ngoai vong lap
# Khong tot: moi vong lap doc lai gia tri tu bo nho (co the chua cache)
total = 0
for i in range(1_000_000):
    total += data[i] * multiplier  # multiplier co the cache duoc

# Tot hon: gan vao bien cuc bo (compiler/CPU co the giu trong thanh ghi)
m = multiplier
total = 0
for i in range(1_000_000):
    total += data[i] * m

Sự khác biệt ở đây không phải số dòng — mà là số lần pha Execute phải ra ngoài RAM để lấy multiplier.

7. Liên hệ các bài khác

  • Bài 01 — Mô hình von Neumann: chu kỳ fetch-decode-execute là vòng lặp lõi của máy stored-program — PC, IR và bus bộ nhớ đều xuất hiện trong sơ đồ von Neumann đó.
  • Bài 03 — Register, ALU và Control Unit: các thành phần này tham gia cụ thể vào pha decode (control unit giải mã opcode) và execute (ALU tính toán, bus ghi/đọc thanh ghi).
  • Bài 04 — Assembly nhập môn: lệnh assembly chính là nội dung CPU fetch vào IR rồi decode — hiểu opcode và toán hạng ở bài này giúp đọc assembly tự nhiên hơn.

8. Tóm tắt

  • Fetch-decode-execute là vòng lặp cốt lõi mọi CPU lặp hàng tỷ lần mỗi giây — không có ngoại lệ.
  • Program counter (PC) trỏ tới lệnh tiếp theo; tự động tăng sau mỗi Fetch, và bị ghi đè bởi lệnh nhảy.
  • Instruction register (IR) giữ tạm lệnh vừa nạp từ RAM trong khi Decode và Execute xử lý.
  • Control unit giải mã opcode + toán hạng, điều phối ALU, bus bộ nhớ, hoặc ghi đè PC tùy loại lệnh.
  • Lệnh nhảy (JUMP/BRANCH) ghi đè PC — đây là cơ chế nền tảng của if, while, gọi hàm.
  • Một lệnh thực tế tốn nhiều chu kỳ clock, không phải một; truy cập RAM tốn nhiều hơn phép tính ALU rất nhiều.
  • Số dòng code không đo được tốc độ — số chu kỳ clock thực tế mới là thước đo; hiểu pha Execute giúp bạn tư duy đúng về bottleneck.

9. Tự kiểm tra

Tự kiểm tra
Q1
Program counter (PC) thay đổi như thế nào sau pha Fetch, và điều gì xảy ra với PC khi CPU gặp lệnh nhảy (JUMP)?
Sau mỗi pha Fetch, PC tự động tăng thêm kích thước một lệnh (ví dụ 4 byte) để trỏ sang lệnh tiếp theo trong dãy. Khi CPU gặp lệnh JUMP, pha Execute ghi đè PC bằng địa chỉ đích của lệnh nhảy — thay vì tăng tuyến tính. Lần Fetch tiếp theo do đó nạp lệnh tại địa chỉ đích, không phải lệnh kề tiếp. Đây là cơ chế nền tảng mọi điều khiển luồng (if, vòng lặp, gọi hàm) ở tầng máy.
Q2
Vì sao chạy một lệnh LOAD từ RAM có thể tốn nhiều chu kỳ clock hơn một lệnh ADD?
Lệnh ADD chỉ dùng ALU — phép tính số học hoàn thành trong 1 chu kỳ clock, dữ liệu nằm sẵn trong thanh ghi. Lệnh LOAD phải ra ngoài bus bộ nhớ để lấy dữ liệu từ RAM — RAM chậm hơn CPU nhiều bậc (hàng chục đến hàng trăm chu kỳ chờ). CPU phải dừng (stall) cho đến khi dữ liệu truyền về. Đây là lý do cache L1/L2/L3 tồn tại: giữ dữ liệu dùng thường xuyên gần CPU để giảm thời gian chờ LOAD.
Q3
Nếu CPU chạy 3 GHz, điều đó có nghĩa là nó thực thi 3 tỷ lệnh mỗi giây không? Giải thích.
Không. 3 GHz nghĩa là 3 tỷ chu kỳ clock mỗi giây, không phải 3 tỷ lệnh. Một lệnh thường tốn nhiều chu kỳ clock: lệnh LOAD có thể tốn 10–100+ chu kỳ, lệnh phức tạp tốn vài chu kỳ. CPU pipeline hiện đại có thể hoàn thành trung bình nhiều hơn 1 lệnh/chu kỳ nhờ superscalar, nhưng đó là trung bình — không phải mọi lệnh chạy trong 1 chu kỳ. Kết luận: clock speed là điều kiện cần, không phải thước đo duy nhất của hiệu năng.
Q4
Tại sao đếm số dòng code không giúp bạn đánh giá tốc độ chương trình?
Vì mỗi dòng code bậc cao dịch ra số lượng lệnh máy và số chu kỳ clock rất khác nhau. Một dòng a = b + c có thể chỉ cần 1–2 lệnh máy và 1–2 chu kỳ. Một dòng result = file.read() có thể cần hàng nghìn lệnh và hàng triệu chu kỳ chờ I/O. Tốc độ thực sự nằm ở tổng số chu kỳ clock — đặc biệt tốn kém ở pha Execute khi lệnh LOAD/STORE đụng RAM chậm.
Q5
Vai trò của instruction register (IR) trong chu kỳ fetch-decode-execute là gì? Nó khác program counter (PC) thế nào?
PC là con trỏ — lưu địa chỉ của lệnh tiếp theo cần nạp, không chứa nội dung lệnh. IR là bộ nhớ tạm — lưu nội dung (các bit) của lệnh vừa Fetch từ RAM, giữ nguyên đó cho Decode và Execute dùng. Tương tự analogy dây chuyền: PC là "số thứ tự phiếu đang đọc", IR là "tờ phiếu đang cầm trên tay". Sau khi Execute xong, IR bị ghi đè bởi lệnh kế tiếp trong vòng lặp mới.
Q6
Một lập trình viên viết vòng lặp tính tổng 1 triệu phần tử, và đặt một phép đọc hằng số bên ngoài vòng lặp thay vì đọc lại mỗi lần. Điều này ảnh hưởng đến pha nào trong chu kỳ fetch-decode-execute và tại sao lại nhanh hơn?
Tối ưu này ảnh hưởng đến pha Execute. Khi hằng số nằm trong biến cục bộ (hoặc compiler/CPU giữ trong thanh ghi), mỗi vòng lặp thực thi lệnh ADD với dữ liệu đã sẵn trong thanh ghi — tốn 1 chu kỳ. Nếu đọc lại từ bộ nhớ mỗi lần, mỗi vòng cần thêm một lệnh LOAD có thể tốn hàng chục chu kỳ chờ RAM. Nhân lên 1 triệu lần, tiết kiệm được hàng chục triệu chu kỳ — hiệu năng tăng rõ rệt mà không thay đổi logic chương trình.

Bài tiếp theo: Register, ALU và control unit

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

Đặt 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