Dữ liệu & CPU/Register, ALU và control unit — bên trong CPU
11/23
Bài 11 / 23~14 phútMáy chạy thế nàoMiễn phí lượt xem

Register, ALU và control unit — bên trong CPU

Register là gì và vì sao nhanh hơn RAM hàng trăm lần, ALU làm phép tính ra sao, control unit điều phối thế nào. Vì sao giữ dữ liệu nóng trong register quan trọng.

TL;DR: CPU không lấy dữ liệu trực tiếp từ RAM để tính toán — nó copy dữ liệu vào register (ô nhớ siêu nhỏ ngay trong chip, truy cập trong 1 chu kỳ) rồi mới tính. ALU (Arithmetic Logic Unit) thực hiện phép cộng, trừ, AND, OR, shift và đặt kết quả vào flags register để lệnh nhảy có điều kiện đọc. Control unit giải mã lệnh và phát tín hiệu điều phối toàn bộ. Hiểu ba thành phần này giải thích vì sao compiler cố nhét biến vào register, vì sao -O2 nhanh hơn, và vì sao "register pressure" là khái niệm thực sự quan trọng.

Bạn chạy một vòng for với biến đếm i cộng lên mỗi iteration. Trực giác nói rằng CPU "lấy i từ RAM, cộng 1, rồi cất lại". Nhưng nếu RAM mỗi lần truy cập mất hàng chục đến hàng trăm chu kỳ, vòng lặp đơn giản nhất cũng sẽ vô cùng chậm. Thực tế thì không — vì CPU có một lớp bộ nhớ nhỏ hơn, nằm ngay trong chip, nhanh đến mức truy cập tốn đúng 1 chu kỳ xung nhịp.

Bài này giải thích ba thành phần cốt lõi bên trong CPU — register, ALU, control unit — và cơ chế phối hợp giữa chúng khi thực thi một phép tính đơn giản như a = b + c.

1. Analogy — bàn làm việc và kho hồ sơ

Hình dung một kế toán viên làm việc trong văn phòng. Anh ta có một bàn làm việc nhỏ trước mặt — chỉ đặt được vài tờ giấy cùng lúc, nhưng lấy bất kỳ tờ nào cũng tức thì, không cần đứng dậy. Phía sau văn phòng là kho hồ sơ — chứa được hàng triệu tài liệu, nhưng mỗi lần cần một file, anh ta phải đứng dậy, đi vào kho, tìm, mang ra — mất thêm thời gian.

Kế toán chỉ có thể tính toán trên những gì đang ở trên bàn. Mọi thứ trong kho phải được mang ra bàn trước, tính xong rồi cất lại.

Văn phòngCPU
Mặt bàn (vài tờ giấy, lấy tức thì)Register (vài chục ô, truy cập 1 chu kỳ)
Kho hồ sơ (hàng triệu file, lấy lâu hơn)RAM (hàng tỷ byte, truy cập hàng chục–hàng trăm chu kỳ)
Số tờ bàn chứa đượcSố register (tài nguyên khan hiếm)
Kế toán viênControl unit + ALU
Lấy file từ kho ra bànLệnh load (RAM → register)
Cất file từ bàn vào khoLệnh store (register → RAM)
💡 Cách nhớ

Register là "mặt bàn" của CPU — ít chỗ, nhưng tính toán chỉ xảy ra ở đây. RAM là "kho hồ sơ" — nhiều chỗ, nhưng phải load ra register trước mới dùng được.

2. Register — ô nhớ siêu nhanh ngay trong chip

Register là các ô nhớ nhỏ được tích hợp trực tiếp vào lõi CPU, nằm cạnh ALU. Truy cập một register tốn đúng 1 chu kỳ xung nhịp — không cần đi qua bus nào, không cần địa chỉ bộ nhớ, không chờ đợi.

So sánh tốc độ truy cập (CPU 3 GHz hiện đại, xấp xỉ):

Tầng bộ nhớLatency điển hìnhQuy đổi chu kỳ
Register0.3 ns1 chu kỳ
L1 cache1–4 ns3–12 chu kỳ
L2 cache4–12 ns12–36 chu kỳ
RAM (DRAM)60–100 ns180–300 chu kỳ

Vì sao không làm thật nhiều register cho tiện? Vì tích hợp register trong chip rất đắt về diện tích bán dẫn — mỗi register cần mạch flip-flop tốc độ cao. CPU x86-64 có 16 general-purpose register (rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, r8–r15), mỗi cái 64-bit. Đây là tài nguyên khan hiếm mà compiler phải phân bổ cẩn thận.

Phân loại register chính

General-purpose register (rax, rbx, ..., r15): chứa dữ liệu và địa chỉ tạm thời trong tính toán. Tên gọi như "general-purpose" nhưng thực tế một số có convention riêng (rax thường trả về giá trị hàm, rsp là stack pointer theo convention ABI).

Program Counter (PC) — còn gọi là Instruction Pointer (rip trên x86-64): chứa địa chỉ của lệnh tiếp theo sẽ được fetch. Mỗi lần fetch xong, PC tự động tăng lên; lệnh nhảy ghi một địa chỉ mới vào PC để thay đổi luồng thực thi.

Stack Pointer (SP / rsp): chứa địa chỉ đỉnh stack hiện tại. Lệnh pushpop đọc/ghi SP tự động. Calling convention dùng SP để truyền tham số và lưu return address.

Flags register (EFLAGS / RFLAGS): tập hợp các bit cờ do ALU đặt sau mỗi phép tính — cờ Zero (ZF) bật khi kết quả bằng 0, cờ Carry (CF) bật khi có nhớ ra ngoài bit cao nhất, cờ Overflow (OF) bật khi kết quả tràn signed, cờ Sign (SF) phản ánh dấu kết quả. Các lệnh nhảy có điều kiện như je (jump if equal) hay jg (jump if greater) đọc flags register để quyết định có nhảy không.

3. ALU — đơn vị thực hiện phép tính

ALU (Arithmetic Logic Unit) là mạch tổ hợp thực hiện hai nhóm phép toán:

Số học: cộng (add), trừ (sub), nhân (imul), chia (idiv). Phép cộng nhị phân là phép toán cơ bản nhất — mạch cộng nhị phân (full adder) xếp nối tiếp hoặc song song tạo thành ALU.

Logic: AND, OR, XOR, NOT và các phép shift (shl, shr, sar). Đây chính là các phép thao tác bit bạn đã học ở Module 1 — ALU là nơi chúng thực sự xảy ra ở mức phần cứng.

Sau mỗi phép tính, ALU tự động đặt cờ vào flags register:

Tinh a + b:
  Neu ket qua = 0  -> dat ZF = 1
  Neu tran unsigned -> dat CF = 1
  Neu tran signed   -> dat OF = 1
  Bit cao ket qua  -> dat SF

Các cờ này là cách ALU "báo cáo" cho control unit và lệnh nhảy biết kết quả có đặc biệt không — mà không cần so sánh riêng một lần nữa.

flowchart LR
  A["Register A\n(operand 1)"] --> ALU["ALU"]
  B["Register B\n(operand 2)"] --> ALU
  ALU --> C["Register ket qua"]
  ALU --> F["Flags register\n(ZF, CF, OF, SF)"]

4. Control unit — bộ điều phối

Control unit không tính toán — nó đọc lệnh đã được decode và phát tín hiệu điều khiển đến mọi thành phần khác: "ALU hãy cộng", "register R1 hãy xuất giá trị lên bus", "register R3 hãy ghi giá trị từ bus vào".

Quy trình một lệnh đơn giản add r1, r2 (r1 = r1 + r2):

Buoc 1 (Fetch): doc byte lenh tu dia chi trong PC, nap vao IR
Buoc 2 (Decode): control unit giai ma IR -> biet day la "add", nguon la r2, dich la r1
Buoc 3 (Execute): phat tin hieu:
    - r1, r2 -> ALU input
    - ALU thuc hien cong
    - ALU output -> ghi vao r1
    - Cap nhat Flags register
Buoc 4: PC tang len -> chuan bi fetch lenh ke tiep

Control unit trên CPU hiện đại là mạch rất phức tạp — nó không chỉ decode một lệnh mà còn quản lý pipeline (chạy song song nhiều lệnh ở các giai đoạn khác nhau), branch prediction, và out-of-order execution. Nhưng nguyên lý cốt lõi vẫn là: decode instruction → phát tín hiệu → phối hợp register và ALU.

5. Từ code bậc cao xuống register và ALU

Một phép gán đơn giản a = b + c trong ngôn ngữ bậc cao ánh xạ xuống chuỗi ba lệnh máy:

; Gia su b o [rbp-8], c o [rbp-12], ket qua can luu vao a o [rbp-4]
mov  eax, DWORD PTR [rbp-8]   ; load b tu RAM vao register eax
add  eax, DWORD PTR [rbp-12]  ; ALU: eax = eax + c (doc c tu RAM truc tiep)
mov  DWORD PTR [rbp-4], eax   ; store ket qua tu eax vao a tren stack

Ba bước: load (RAM → register), ALU operation (register → ALU → register), store (register → RAM). Đây là mẫu lặp đi lặp lại trong mọi chương trình — load/store là cầu nối giữa RAM chậm và register nhanh, ALU chỉ đọc và ghi register.

Nếu bc đã nằm trong register từ lệnh trước (compiler giữ chúng ở đó), không cần load nữa — tiết kiệm hàng trăm chu kỳ mỗi lần.

6. Đào sâu (tuỳ chọn)

📚 Đào sâu (tuỳ chọn)

Register renaming và register vật lý: x86-64 chỉ có 16 architectural register (tên bạn thấy trong assembly). Nhưng CPU hiện đại (như Intel Skylake) có hàng trăm physical register bên dưới. Kỹ thuật register renaming ánh xạ architectural register sang physical register khác nhau cho từng lệnh — cho phép CPU chạy nhiều lệnh song song (out-of-order execution) mà không bị giả phụ thuộc (WAR/WAW hazard). Đây là lý do tại sao modern CPU chạy nhanh hơn rất nhiều so với kiến trúc đơn giản.

Flags register và lệnh nhảy có điều kiện: lệnh so sánh cmp a, b thực ra là phép trừ a - b nhưng chỉ đặt cờ, không ghi kết quả. Sau đó jne (jump if not equal) đọc ZF: nếu ZF = 0 (kết quả khác 0, tức a khác b) thì nhảy. Toàn bộ cấu trúc if/else và vòng lặp trong code bậc cao đều biên dịch xuống chuỗi cmp + lệnh nhảy dựa trên flags.

Calling convention và register: ABI (Application Binary Interface) quy định register nào giữ tham số và giá trị trả về. Trên x86-64 Linux (System V ABI): 6 tham số đầu vào rdi, rsi, rdx, rcx, r8, r9; giá trị trả về vào rax. Compiler tuân theo ABI này để các function compiled riêng có thể gọi nhau. Bài 04 — Assembly nhập môn sẽ đào sâu hơn.

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

Tại sao compiler tối ưu giữ biến nóng trong register. Khi bạn bật -O1 hoặc -O2, compiler chạy thuật toán register allocation — phân tích biến nào được dùng nhiều nhất ("hot") và cố giữ chúng trong register suốt vòng đời. Biến đếm vòng lặp là ví dụ điển hình: thay vì load từ stack mỗi iteration, compiler giữ nó trong register xuyên suốt vòng lặp.

// Vong lap don gian -- bien dem i rat hot
for (int i = 0; i < n; i++) {
    sum += arr[i];
}
// Compiler -O2: i nam trong register xuyen suot, khong load/store i moi iteration
// Khong toi uu: i load/store RAM moi lan (ton ~100-300 chu ky moi iteration)

Register pressure là gì. Khi có quá nhiều biến "sống" cùng lúc (live range overlap) vượt quá số register có sẵn (16 trên x86-64, nhưng thực tế ít hơn vì một số bị dùng cho stack/return/ABI), compiler buộc phải spill — cất biến tạm thời ra stack (RAM) rồi load lại khi cần. Spill làm chậm đáng kể.

// Ap luc register cao -- nhieu bien song song
double a, b, c, d, e, f, g, h, x, y, z;  // ~11 bien float song cung luc
// Neu vuot so register FPU/XMM san co, compiler spill mot so ra stack
// -> load/store them -> chay cham hon du khong co gi phuc tap

Vì sao biến cục bộ nhỏ gọn thường nhanh. Ít biến đồng thời = ít register pressure = ít spill = ít load/store. Bạn không cần tối ưu register thủ công — compiler làm tốt hơn bạn — nhưng hiểu cơ chế giúp bạn giải thích tại sao function nhỏ, ít biến, thường chạy nhanh hơn function dài với nhiều biến trung gian.

💡 Không cần tối ưu tay

Compiler hiện đại (GCC, Clang, javac + JIT) làm register allocation tốt hơn hầu hết lập trình viên. Bạn không cần khai báo register int i hay viết tay assembly. Hiểu register allocation giúp bạn đọc hiểu tại sao -O2 nhanh hơn, không phải để làm thủ công.

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

  • Bài 02 — Chu kỳ fetch-decode-execute: control unit và register đóng vai trò trung tâm trong chu kỳ fetch-decode-execute — PC là register điều phối luồng lệnh, IR chứa lệnh đang decode. Bài này và bài 02 bổ trợ nhau.
  • Bài 04 — Assembly nhập môn: bạn sẽ thấy tên register thực tế (rax, rdi, rsp...) và cách ALU operation xuất hiện trong assembly (add, sub, xor, shl). Đọc bài 03 trước giúp bài 04 có nền.
  • Module 1 — Thao tác bit: các phép AND/OR/XOR/shift bạn đã học ở Module 1 là chính xác những gì ALU thực hiện ở mức phần cứng. Bài này cụ thể hoá "ai" chạy các phép đó.

9. Tóm tắt

  • Register là ô nhớ tích hợp ngay trong CPU, truy cập 1 chu kỳ — nhanh hơn RAM hàng trăm lần. x86-64 có 16 general-purpose register.
  • Bốn loại register quan trọng: general-purpose (dữ liệu tạm), Program Counter (địa chỉ lệnh tiếp theo), Stack Pointer (đỉnh stack), Flags register (cờ kết quả ALU).
  • ALU thực hiện số học (cộng, trừ) và logic (AND, OR, XOR, shift) rồi đặt kết quả vào flags register — các cờ ZF, CF, OF, SF.
  • Control unit giải mã lệnh và phát tín hiệu điều phối register, ALU, bus — không tự tính toán.
  • Mẫu cơ bản: load (RAM → register) → ALU operation → store (register → RAM).
  • Compiler cố giữ biến "nóng" trong register (register allocation); khi vượt số register, spill ra stack làm chậm.
  • Hiểu register giải thích vì sao -O2 nhanh hơn và vì sao biến cục bộ ít thường nhanh hơn.

10. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao CPU cần register thay vì đọc thẳng từ RAM mỗi khi tính toán?
RAM có latency hàng chục đến hàng trăm chu kỳ mỗi lần truy cập — nếu mỗi phép cộng phải đợi RAM, CPU 3 GHz sẽ dành phần lớn thời gian chờ, không tính toán. Register nằm ngay trong chip, cạnh ALU, truy cập tốn đúng 1 chu kỳ. Vì vậy CPU copy dữ liệu cần thiết vào register trước, tính toán trên register, rồi mới ghi kết quả ngược lại RAM khi cần. Đây là lý do tại sao lệnh loadstore tồn tại — chúng là cầu nối giữa hai thế giới tốc độ khác nhau hàng trăm lần.
Q2
Flags register là gì và lệnh nhảy có điều kiện dùng nó thế nào?
Flags register là tập hợp các bit cờ mà ALU tự động đặt sau mỗi phép tính: ZF (kết quả bằng 0), CF (tràn unsigned), OF (tràn signed), SF (kết quả âm). Lệnh so sánh cmp a, b thực ra tính a - b và chỉ đặt cờ, không lưu kết quả. Lệnh nhảy như je (jump if equal) sau đó đọc ZF: nếu ZF = 1 (tức a bằng b) thì nhảy, ngược lại tiếp tục. Toàn bộ if/else và vòng lặp trong code bậc cao đều biên dịch thành chuỗi cmp + lệnh nhảy dựa trên flags như thế này.
Q3
Control unit làm gì khác với ALU?
ALU thực hiện tính toán — cộng, trừ, AND, OR, shift — nó là mạch tổ hợp đưa ra kết quả số học hoặc logic. Control unit không tính toán — nó đọc lệnh đã được fetch vào Instruction Register, giải mã xem đó là lệnh gì, rồi phát tín hiệu điều khiển để các thành phần khác làm đúng việc của mình: ra lệnh cho ALU cộng, ra lệnh cho register xuất giá trị, ra lệnh cho bus truyền dữ liệu. Control unit là "nhạc trưởng" điều phối, ALU là "nhạc công" tính toán.
Q4
"Register pressure" là gì và hệ quả khi xảy ra?
Register pressure xảy ra khi số biến cần dùng cùng lúc (các biến có live range chồng nhau) vượt quá số register vật lý sẵn có. Compiler phải spill — cất một số biến tạm thời ra stack (là RAM), rồi load lại khi cần. Mỗi lần spill/reload tốn hàng chục đến hàng trăm chu kỳ, thay vì 1 chu kỳ nếu ở trong register. Hệ quả là code chạy chậm hơn dù logic không phức tạp. Function nhỏ ít biến có register pressure thấp — ít spill, ít chờ RAM.
Q5
Một phép gán `a = b + c` ánh xạ xuống bao nhiêu lệnh máy? Hãy liệt kê và giải thích từng bước.
Ba lệnh chính: (1) mov eax, [b] — load giá trị b từ RAM (hoặc stack) vào register eax, (2) add eax, [c] — ALU cộng eax với c, kết quả ghi lại vào eax và đặt flags, (3) mov [a], eax — store giá trị trong eax ra vị trí lưu a trên RAM/stack. Đây là mẫu load–operate–store xuất hiện trong mọi phép tính. Nếu b hoặc c đã nằm sẵn trong register từ lệnh trước (compiler giữ lại), bước load tương ứng được bỏ qua — tiết kiệm hàng trăm chu kỳ mỗi lần.
Q6
Vì sao bật `-O2` khi compile thường làm code chạy nhanh hơn đáng kể?
Một trong những tối ưu quan trọng nhất của -O2register allocation tốt hơn. Compiler phân tích biến nào được dùng nhiều (hot variables) và giữ chúng trong register xuyên suốt thay vì load/store mỗi lần. Ví dụ biến đếm vòng lặp ở chế độ không tối ưu có thể bị load từ stack và store lại mỗi iteration — tốn hàng trăm chu kỳ. Với -O2, biến đó sống trong register cả vòng lặp, tốn 1 chu kỳ mỗi lần cộng. Ngoài register allocation, -O2 còn làm loop unrolling, inlining, constant folding — nhưng tất cả đều liên quan đến việc giảm số lần load/store không cần thiết.
Q7
x86-64 có 16 general-purpose register nhưng tại sao con số thực tế sẵn dùng ít hơn trong một hàm thông thường?
Theo calling convention (System V ABI trên Linux x86-64), một số register có công dụng cố định hoặc bị bảo lưu: rsp là stack pointer (không dùng tự do), rbp thường là frame pointer, rax trả về giá trị hàm, rdi/rsi/rdx/rcx/r8/r9 truyền tham số vào hàm được gọi. Ngoài ra có phân biệt caller-saved (hàm gọi phải tự save/restore nếu cần) và callee-saved (hàm được gọi phải tự save/restore). Quản lý ABI tiêu thụ một số register, nên compiler thực tế chỉ có khoảng 6–10 register thực sự tự do cho biến cục bộ trong một hàm thông thường.

Bài tiếp theo: Assembly nhập môn

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