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òng | CPU |
|---|---|
| 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 được | Số register (tài nguyên khan hiếm) |
| Kế toán viên | Control unit + ALU |
| Lấy file từ kho ra bàn | Lệnh load (RAM → register) |
| Cất file từ bàn vào kho | Lệnh store (register → RAM) |
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ình | Quy đổi chu kỳ |
|---|---|---|
| Register | 0.3 ns | 1 chu kỳ |
| L1 cache | 1–4 ns | 3–12 chu kỳ |
| L2 cache | 4–12 ns | 12–36 chu kỳ |
| RAM (DRAM) | 60–100 ns | 180–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 push và pop đọ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 b và c đã 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)
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.
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
-O2nhanh hơn và vì sao biến cục bộ ít thường nhanh hơn.
10. Tự kiểm tra
Q1Vì sao CPU cần register thay vì đọc thẳng từ RAM mỗi khi tính toán?▸
load và store 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.Q2Flags register là gì và lệnh nhảy có điều kiện dùng nó thế nào?▸
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.Q3Control unit làm gì khác với ALU?▸
Q4"Register pressure" là gì và hệ quả khi xảy ra?▸
Q5Mộ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.▸
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.Q6Vì sao bật `-O2` khi compile thường làm code chạy nhanh hơn đáng kể?▸
-O2 là register 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.Q7x86-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?▸
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
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