Vì sao CPU hiện đại nhanh đến vậy?
TinyCPU cũng lấy lệnh, giải mã và thực thi. Vậy CPU hiện đại làm gì khác để nhanh đến vậy?
Ở bài trước, TinyCPU chạy chương trình:
LOAD 5
ADD 3
STORE 8
Mỗi lệnh đi qua ba bước Fetch → Decode → Execute. Ba lệnh cần 9 bước.
CPU trong laptop của bạn về nguyên lý vẫn phải lấy lệnh, hiểu lệnh và thực hiện lệnh.
Vậy tại sao nó chạy được những chương trình lớn hơn TinyCPU rất nhiều mà vẫn phản hồi gần như tức thì?
Câu trả lời không phải vì CPU đã bỏ vòng lấy lệnh, giải mã, thực thi. Mà vì CPU hiện đại tìm cách không đứng chờ.
-
Bạn bấm từng bước xung nhịp
TinyCPU bấmF bấmD bấmE
Bây giờ
Nhịp tự chạy, hàng tỷ lần mỗi giây.
-
Cần dữ liệu thì đi lấy từ RAM cache
TinyCPU CPU chờ RAM
Bây giờ CPU cache RAM
Dữ liệu ở cache thì khỏi đi tới RAM.
-
Xong hẳn lệnh 1 mới sang lệnh 2 pipeline
TinyCPU9 chu kỳ
Lệnh 1 FDE Lệnh 2 FDE Lệnh 3 FDEBây giờ5 chu kỳ
Lệnh 1 FDE Lệnh 2 FDE Lệnh 3 FDEF lấy lệnh D giải mã E thực thi
-
Chỉ có một nơi xử lý nhiều nhân
TinyCPU A→ B→ C→ D
Bây giờ1 chu kỳ
Nhân 1 A Nhân 2 B Nhân 3 C Nhân 4 DBốn việc độc lập chạy cùng một lúc, không phải xếp hàng.
TinyCPU đang phí thời gian ở đâu?
Nhìn kỹ TinyCPU, ta thấy bốn vấn đề.
1. Mạch cần biết khi nào được chuyển sang bước tiếp theo
ALU không cho ra kết quả ngay lập tức. Tín hiệu điện phải đi qua nhiều transistor và cổng logic trước khi kết quả ổn định.
CPU cần một nhịp chung để biết:
Bây giờ kết quả đã sẵn sàng. Có thể chép nó vào thanh ghi.
Đó là việc của clock.
2. CPU cần dữ liệu nhưng RAM ở xa
Nếu lệnh cần một giá trị trong RAM, CPU có thể phải đợi rất lâu so với tốc độ của chính nó.
Đó là lý do có cache.
3. TinyCPU chỉ làm một giai đoạn tại một thời điểm
Khi ALU đang Execute, phần Fetch không làm gì. Khi CPU Decode, ALU lại đang rảnh.
Đó là thứ pipeline cố tận dụng.
4. Một CPU không nhất thiết phải làm mọi việc một mình
Nếu có nhiều việc độc lập, nhiều bộ xử lý có thể làm chúng cùng lúc.
Đó là lý do CPU có nhiều core.
Bốn ý này là phần lớn câu chuyện của bài: clock, cache, pipeline, nhiều core. Ta xem từng cái.
Clock: ai bảo CPU chuyển sang bước tiếp theo?
Lấy phép cộng 5 + 3. Khi tín hiệu đi vào ALU, kết quả 8 không xuất hiện ngay lập tức.
Một số đường tín hiệu ngắn hơn. Một số phải đi qua nhiều cổng logic hơn. Trong một khoảng thời gian rất ngắn, đầu ra có thể vẫn đang thay đổi.
Nếu R0 chép kết quả quá sớm, nó có thể giữ một giá trị sai.
CPU vì vậy có một tín hiệu lên xuống đều gọi là clock (xung nhịp). Có thể hình dung nó như tiếng trống:
nhịp 1 nhịp 2 nhịp 3 nhịp 4
│ │ │ │
▼ ▼ ▼ ▼
thanh ghi thanh ghi thanh ghi thanh ghi
nhận số nhận số nhận số nhận số
Giữa hai nhịp, các mạch có thời gian để tính toán. Tới nhịp tiếp theo, các thanh ghi như PC hay R0 mới nhận giá trị mới.
Trong TinyCPU, mỗi lần bạn bấm “Chạy một bước” gần giống như bạn đang tự tạo một nhịp cho máy. Trong CPU thật, clock làm việc đó tự động.
GHz nghĩa là gì?
Tần số clock được đo bằng hertz (Hz):
1 Hz = 1 nhịp mỗi giây
1 MHz = 1 triệu nhịp mỗi giây
1 GHz = 1 tỷ nhịp mỗi giây
Một CPU chạy ở 3 GHz có khoảng 3 tỷ nhịp mỗi giây.
Ở 1 GHz, mỗi nhịp dài khoảng 1 ns. Ở 5 GHz, mỗi nhịp chỉ còn khoảng 0,2 ns. Một nanogiây (ns) là một phần tỷ giây.
Thử mô phỏng dưới đây. Tăng dần từ 1 Hz lên 5 GHz và để ý độ dài mỗi nhịp.
FETCH nhịp 1 · mỗi pha một nhịp (minh hoạ) Quá nhanh để nhìn: 100 nhịp mỗi giây · mỗi nhịp 1 s
Clock càng nhanh, CPU càng có nhiều cơ hội cập nhật trạng thái mỗi giây.
Nhưng có một giới hạn. Mạch phải có đủ thời gian để ổn định trước nhịp tiếp theo. Nếu ép clock quá nhanh, mạch chưa tính xong mà CPU đã chuyển bước.
Ngoài ra, tăng tần số thường làm chip tiêu thụ nhiều điện và sinh nhiều nhiệt hơn.
Clock giải quyết chuyện khi nào được chuyển bước.
Nhưng có một vấn đề khác còn lớn hơn: CPU chạy rất nhanh, còn RAM thì không nhanh bằng.
Cache: đừng chạy ra RAM mỗi lần cần dữ liệu
Giả sử CPU cần đọc một giá trị X.
Nếu X nằm trong RAM, CPU phải gửi yêu cầu ra ngoài chip rồi chờ dữ liệu quay lại. Đối với CPU, khoảng chờ đó rất dài.
Hình dung một đầu bếp cực nhanh, nhưng nguyên liệu lại nằm trong kho ở cuối hành lang. Dù đầu bếp nhanh tới đâu, mỗi lần chạy đi lấy một quả trứng vẫn phải chờ.
Giải pháp đơn giản:
Để những thứ thường dùng ngay cạnh đầu bếp.
Trong CPU, vùng nhớ nhỏ và rất nhanh đó gọi là cache (bộ nhớ đệm). Thông thường ta gặp:
| Bộ nhớ | Đặc điểm |
|---|---|
| L1 cache | rất nhỏ, rất nhanh |
| L2 cache | lớn hơn L1, chậm hơn một chút |
| L3 cache | lớn hơn nữa, thường dùng chung giữa nhiều core |
| RAM | lớn hơn rất nhiều nhưng chậm hơn cache |
Khi CPU cần X, nó không chạy thẳng ra RAM. Nó hỏi lần lượt:
L1 có X không? → không
L2 có X không? → không
L3 có X không? → không
RAM có X
Tìm thấy trong cache gọi là cache hit. Không tìm thấy và phải xuống tầng dưới gọi là cache miss.
Thử mô phỏng dưới đây. Chọn nơi X đang nằm, bấm “CPU cần dữ liệu X”, rồi bấm “Chạy lại cùng dữ liệu”.
Dữ liệu X đang nằm ở đâu?
- Thanh ghi trong CPU
- Cache L1
- Cache L2
- Cache L3
- RAM ngoài chip CPU
- SSD lưu lâu dài
CPU hỏi tầng gần nhất trước. Hụt (miss) thì hỏi tầng kế tiếp, có (hit) thì dừng.
Lần đầu có thể phải đi tới RAM. Nhưng sau khi dữ liệu được mang lên cache, lần đọc tiếp theo nhanh hơn rất nhiều.
Vì sao cache có ích đến vậy?
Chương trình thường có hai thói quen.
Thứ nhất, vừa dùng một dữ liệu thì có khả năng sẽ dùng lại nó:
score = score + 1
score = score + bonus
if score > 100 ...
score được dùng liên tục.
Thứ hai, chương trình thường dùng những dữ liệu nằm gần nhau. Vừa đọc array[10] thì khả năng cao sắp tới sẽ đọc array[11], array[12].
Vì vậy CPU thường mang cả một vùng dữ liệu nhỏ vào cache, không chỉ đúng một giá trị.
Cache không làm RAM nhanh hơn. Nó giúp CPU ít phải chờ RAM hơn.
Nhưng dữ liệu về kịp rồi, TinyCPU vẫn còn một chỗ phí: mỗi lúc chỉ một phần của nó có việc.
Pipeline: đừng để các phần của CPU ngồi không
TinyCPU làm như sau:
chu kỳ 1 2 3 4 5 6 7 8 9
Lệnh 1 F D E
Lệnh 2 F D E
Lệnh 3 F D E
Nghĩa là phải chạy xong toàn bộ lệnh 1 rồi mới bắt đầu lấy lệnh 2.
Nhưng hãy nhìn kỹ. Khi lệnh 1 đang Execute:
- mạch Fetch đang rảnh
- mạch Decode cũng đang rảnh
Tại sao không cho chúng làm lệnh tiếp theo? Đó là pipeline:
chu kỳ 1 2 3 4 5
Lệnh 1 F D E
Lệnh 2 F D E
Lệnh 3 F D E
Ở chu kỳ thứ ba:
Lệnh 1 đang Execute
Lệnh 2 đang Decode
Lệnh 3 đang Fetch
Ba phần khác nhau của CPU cùng có việc.
Thử mô phỏng dưới đây. Bấm “Chạy” ở chế độ không pipeline, rồi đổi sang “Có pipeline” và chạy lại.
F lấy lệnh D giải mã E thực thi
| Chu kỳ | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| I1 | F | D | E | |||||||||
| I2 | F | D | E | |||||||||
| I3 | F | D | E | |||||||||
| I4 | F | D | E |
4 lệnh xong sau 12 chu kỳ.
Trong mô phỏng, bốn lệnh mỗi lệnh ba giai đoạn cần 12 chu kỳ khi chạy tuần tự. Pipeline lý tưởng chỉ cần 6 chu kỳ.
Pipeline không làm một lệnh hoàn thành nhanh hơn. Nó giúp nhiều lệnh đi qua CPU cùng lúc.
Giống dây chuyền rửa xe. Một chiếc xe vẫn phải đi qua rửa → lau → hút bụi. Nhưng khi xe đầu sang bước lau, xe thứ hai đã có thể bắt đầu rửa.
Pipeline không phải lúc nào cũng chạy trơn tru
Giả sử có hai lệnh liền nhau (ví dụ minh hoạ, không thuộc TinyCPU):
ADD R1, R2
MUL R1, 10
Lệnh thứ hai cần kết quả của lệnh thứ nhất. Nếu kết quả chưa có, nó phải chờ.
Một vấn đề khác là lệnh nhảy:
nếu x = 0 thì nhảy tới ô 50
CPU có thể đã Fetch vài lệnh phía sau. Nhưng nếu cuối cùng phải nhảy tới ô 50, những lệnh vừa lấy đó không còn dùng được và bị bỏ đi.
CPU hiện đại có rất nhiều kỹ thuật để giảm những lần chờ này. Nhưng ý chính của pipeline rất đơn giản:
Đừng để một bộ phận của CPU ngồi không chỉ vì bộ phận khác đang bận.
Nhưng một pipeline vẫn chỉ là một dòng công việc
Giả sử máy tính đang phải làm bốn việc độc lập: A, B, C, D.
Một core có thể làm:
A → B → C → D
Nhưng nếu bốn việc không phụ thuộc nhau, tại sao không có bốn bộ xử lý nhỏ làm cùng lúc?
Đó chính là ý tưởng của multicore.
Một CPU có thể chứa nhiều core
Một core (nhân) là một bộ xử lý có thể tự chạy lệnh của riêng nó.
Một CPU hiện đại có thể chứa 4, 8, 12, 16 core hoặc nhiều hơn. Ví dụ, một CPU 8 core có tám bộ xử lý làm việc song song bên trong cùng một con chip.
Nếu có bốn việc độc lập:
Core 1 → A
Core 2 → B
Core 3 → C
Core 4 → D
chúng có thể chạy cùng lúc.
Thử mô phỏng dưới đây. Chạy “4 việc độc lập” với 1 nhân rồi 4 nhân, sau đó đổi sang “4 việc nối tiếp”.
Xong sau 2 đơn vị, nhanh gấp 4 lần 1 nhân.
Nhiều core không có nghĩa mọi chương trình đều nhanh lên tương ứng.
Có những việc không thể chia ra
Giả sử:
B cần kết quả của A
C cần kết quả của B
D cần kết quả của C
Ta có A → B → C → D.
Core 2 không thể chạy B khi A chưa xong. Core 3 cũng không thể chạy C trước B. Dù có 100 core, chuỗi này vẫn phải đi lần lượt.
Đó là giới hạn cơ bản của tính toán song song, thường được diễn tả bằng định luật Amdahl:
Nếu một phần chương trình bắt buộc phải chạy tuần tự, phần đó sẽ giới hạn mức tăng tốc tối đa, dù ta thêm bao nhiêu core.
Định luật Amdahl
Nếu α là phần công việc bắt buộc chạy tuần tự và p là số bộ xử lý:
speedup = 1 / (α + (1 - α) / p)Ví dụ, nếu 10% chương trình không thể chạy song song, thì kể cả có vô hạn core, tốc độ cũng chỉ tăng tối đa khoảng 10 lần.
Nhiều core vì vậy hữu ích nhất khi hệ điều hành hoặc chương trình có nhiều công việc độc lập.
Bốn cách cùng giải một vấn đề
Quay về TinyCPU:
| Vấn đề | CPU hiện đại làm gì? |
|---|---|
| Không biết khi nào kết quả đã ổn định | dùng clock để đồng bộ |
| CPU phải chờ RAM | giữ dữ liệu gần CPU bằng cache |
| Các phần Fetch, Decode, Execute thay nhau ngồi không | cho nhiều lệnh chồng lên nhau bằng pipeline |
| Một bộ xử lý phải làm quá nhiều việc | dùng nhiều core |
Không cơ chế nào tự mình tạo ra một CPU nhanh. Chúng phối hợp với nhau. Một CPU hiện đại có thể cùng lúc:
- chạy clock hàng tỷ nhịp mỗi giây
- tìm phần lớn dữ liệu ngay trong cache
- có nhiều lệnh đang nằm trong pipeline
- chạy nhiều việc trên nhiều core
Đó là lý do hai CPU cùng 4 GHz vẫn có thể cho hiệu năng rất khác nhau.
CPU hiện đại còn làm nhiều hơn thế
Thực tế, CPU ngày nay phức tạp hơn bốn ý trong bài rất nhiều. Chúng còn có thể:
- thực thi nhiều lệnh trong cùng một nhịp
- đoán trước hướng của lệnh nhảy
- đổi thứ tự thực thi nếu việc đó giúp bớt chờ
- có nhiều khối tính toán hoạt động song song
- tự tăng hoặc giảm tần số tuỳ nhiệt độ và tải
Nhưng tất cả đều xoay quanh một mục tiêu quen thuộc:
Giữ cho càng nhiều phần của CPU có việc càng tốt, và giảm thời gian chúng phải đứng chờ.
Từ 5 + 3 tới CPU hiện đại
Khoá học bắt đầu bằng một câu hỏi rất nhỏ:
Khi ta bấm
5 + 3, bên trong máy tính xảy ra chuyện gì?
Ta đã đi từ:
điện bật / tắt
↓
0 và 1
↓
transistor
↓
cổng logic
↓
ALU
↓
thanh ghi
↓
mã máy
↓
Fetch → Decode → Execute
↓
clock, cache, pipeline, nhiều core
- Hai trạng thái điện → bit 0 và 1dễ phân biệt, khó nhầm vì nhiễu
- Relay → đèn điện tửcông tắc điện đầu tiên máy tự bật tắt được
- Transistorcông tắc đủ nhỏ để đặt hàng tỷ cái trên một con chip
- Cổng logicvài transistor nối lại thành một quy tắc
- Mạch cộng → ALUcổng logic ghép lại thì tính được 5 + 3
- Lệnh máychương trình thành chuỗi bit mang opcode và operand
- Fetch → Decode → Executevòng lặp CPU chạy cho từng lệnh
- Clock · cache · pipeline · nhiều nhânlàm vòng lặp đó chạy nhanh
- CPU hiện đạichạy phần mềm bạn dùng mỗi ngày
Mỗi tầng không thay thế tầng trước. Nó được xây trên tầng trước.
CPU hiện đại có thể cực kỳ phức tạp, nhưng cuối cùng vẫn là rất nhiều transistor đóng mở để di chuyển và biến đổi các bit.
Chạy TinyCPU lần cuối
TinyCPU dưới đây vẫn là chiếc máy đơn giản của bài trước, với chương trình LOAD 5, ADD 3, STORE 8.
Chạy lại lệnh ADD 3 từng bước, và để ý PC, IR, ALU và R0.
- 1 LẤY LỆNH Fetch
- 2 GIẢI MÃ Decode
- 3 THỰC THI Execute
BỘ NHỚ
- Phép ALU
- —
- Đầu vào A
- —
- Đầu vào B
- —
- Ghi kết quả
- —
Ba lệnh của chương trình đều đi qua đúng ba bước như vậy, và cuối cùng R0 = 8.
TinyCPU làm từng bước một để bạn nhìn thấy. CPU thật dùng clock để tạo nhịp, cache để bớt chờ dữ liệu, pipeline để chồng nhiều lệnh lên nhau và nhiều core để chạy nhiều việc cùng lúc.
Nhưng tận cùng, nó vẫn làm đúng thứ TinyCPU vừa làm:
lấy lệnh
↓
giải mã
↓
làm việc mà lệnh yêu cầu
Chỉ nhanh hơn rất, rất nhiều.
Giờ khi bấm 5 + 3, bạn đã có thể lần theo câu chuyện từ phép tính trên màn hình xuống tận transistor bên trong CPU.