09/10 Vì sao CPU hiện đại nhanh đến vậy? Các chương

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 FDE

    Bây giờ5 chu kỳ

    Lệnh 1 FDE Lệnh 2 FDE Lệnh 3 FDE

    F 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 D

    Bố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.

Xung nhịp (clock): mỗi nhịp, mạch tiến một bước

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”.

CPU tìm dữ liệu X qua các tầng bộ nhớ

Dữ liệu X đang nằm ở đâu?

  1. Thanh ghi trong CPU
  2. Cache L1
  3. Cache L2
  4. Cache L3
  5. RAM ngoài chip CPU
  6. SSD lưu lâu dài
thời gian chờ

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.

Pipeline: làm xong nhiều lệnh hơn trong cùng số chu kỳ

F lấy lệnh D giải mã E thực thi

Lịch chạy 4 lệnh khi không có pipeline: mỗi lệnh đi hết Fetch, Decode, Execute rồi lệnh sau mới bắt đầu, tổng cộng 12 chu kỳ.
Chu kỳ 123456789101112
I1 FDE
I2 FDE
I3 FDE
I4 FDE

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.

Mặt silicon của CPU AMD Ryzen chụp qua kính hiển vi với các khối lặp lại
Mặt silicon của AMD Ryzen 5 9600X. Các khối giống nhau lặp lại là các core. Ảnh: Fritzchens Fritz / Wikimedia Commons · CC0

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”.

Nhiều nhân (core) có làm mọi việc nhanh gấp nhiều lần?

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
  1. Hai trạng thái điện → bit 0 và 1dễ phân biệt, khó nhầm vì nhiễu
  2. Relay → đèn điện tửcông tắc điện đầu tiên máy tự bật tắt được
  3. Transistorcông tắc đủ nhỏ để đặt hàng tỷ cái trên một con chip
  4. Cổng logicvài transistor nối lại thành một quy tắc
  5. Mạch cộng → ALUcổng logic ghép lại thì tính được 5 + 3
  6. Lệnh máychương trình thành chuỗi bit mang opcode và operand
  7. Fetch → Decode → Executevòng lặp CPU chạy cho từng lệnh
  8. Clock · cache · pipeline · nhiều nhânlàm vòng lặp đó chạy nhanh
  9. 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.

Theo dấu lệnh ADD 3 đi qua CPU
  1. 1 LẤY LỆNH Fetch
  2. 2 GIẢI MÃ Decode
  3. 3 THỰC THI Execute

BỘ NHỚ

00 LOAD 5 0001 0101
01 ADD 3 0010 0011
02 STORE 8 0011 1000
08 dữ liệu 0
Bộ đếm chương trình PC 01 địa chỉ lệnh sắp lấy
Thanh ghi lệnh IR mã thao táctoán hạng
Khối điều khiển Control Unit
Phép ALU
Đầu vào A
Đầu vào B
Ghi kết quả
Khối số học và logic ALU
Thanh ghi R0 5

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.