Mở đầu: một CPU cực nhanh vẫn vô dụng nếu chờ bộ nhớ mãi

Bài 6 xây CPU thực thi ngoài thứ tự cực kỳ tinh vi — nhưng dù có bao nhiêu Reservation Station, mọi lệnh LW/SW (Bài 3) cuối cùng vẫn phải chạm tới bộ nhớ chính (DRAM). Vấn đề: DRAM CHẬM HƠN CPU hàng trăm lần — đây là Memory Wall (bức tường bộ nhớ). Giải pháp không phải làm DRAM nhanh hơn (vật lý không cho phép với giá rẻ), mà là chèn một lớp bộ nhớ SRAM nhỏ, cực nhanh, cực đắt giữa CPU và DRAM — gọi là Cache.


📚 Điều kiện tiên quyết
Nên đọc Bài 3 (truy cập bộ nhớ LW/SW trong datapath — cache bài này chính là lớp ẩn giữa CPU và bộ nhớ chính mà datapath Bài 3 coi là tức thời).

1. Nút thắt cổ chai bộ nhớ (Memory Wall) & Nguyên lý Locality

Tốc độ CPU tăng nhanh hơn tốc độ DRAM suốt nhiều thập kỷ — khoảng cách này chính là Memory Wall. Cache "che giấu" độ trễ DRAM bằng cách khai thác 2 quy luật thống kê gần như LUÔN đúng với chương trình thật:

  • Temporal Locality (cục bộ thời gian) — một địa chỉ VỪA được truy cập có xu hướng được truy cập LẠI trong tương lai gần (vd biến đếm vòng lặp).
  • Spatial Locality (cục bộ không gian) — nếu địa chỉ X vừa được truy cập, các địa chỉ GẦN X (cùng dòng cache) có xu hướng SẮP được truy cập (vd duyệt tuần tự một mảng).
⚠️ Cạm bẫy: duyệt mảng 2D sai chiều phá vỡ Spatial Locality
Mảng 2D lưu theo hàng (row-major, chuẩn C/JS) — duyệt THEO HÀNG truy cập các phần tử NẰM CẠNH NHAU trong bộ nhớ, tận dụng tối đa Spatial Locality. Duyệt THEO CỘT nhảy CÁCH XA mỗi lần (đúng bằng độ dài 1 hàng) — gần như MỌI truy cập đều rơi vào dòng cache KHÁC, phá vỡ hoàn toàn Spatial Locality. Verified thật: mảng 8×8 phần tử 4-byte trên cache 4 dòng×16-byte — duyệt theo HÀNG chỉ 16/64 miss (25%), duyệt theo CỘT MISS TOÀN BỘ 64/64 (100%) — gấp 4 lần tệ hơn, dù tổng số phần tử truy cập giống hệt nhau.

2. Cấu trúc ánh xạ Cache: Direct-Mapped & Set-Associative

Một địa chỉ bộ nhớ được tách thành 3 phần để tra cứu cache (Mục 7.2): Offset (bit thấp nhất, chọn byte TRONG dòng cache), Index (chọn DÒNG/tập trong cache), và Tag (phần còn lại, dùng để XÁC NHẬN đúng dữ liệu đang cần, tránh nhầm với dữ liệu khác cùng index).

split_address.js (trích engine dùng chung cpu-core.js)
function splitAddress(address, offsetBits, indexBits) {
  const offset = address & ((1 << offsetBits) - 1);
  const index = (address >>> offsetBits) & ((1 << indexBits) - 1);
  const tag = address >>> (offsetBits + indexBits);
  return { tag, index, offset };
}
// Verified: splitAddress(0x1234, 4, 2) -> offset=0x4; khu hoi dung dia chi goc

Direct-Mapped: mỗi index ứng với ĐÚNG 1 dòng cache duy nhất — đơn giản, tra cứu cực nhanh, nhưng 2 địa chỉ khác tag CÙNG index sẽ liên tục "đá" nhau ra dù cache còn dư chỗ ở dòng khác (Conflict Miss). Set-Associative N-way: mỗi index ứng với một TẬP chứa N dòng — địa chỉ trùng index nhưng khác tag không còn phải tranh 1 chỗ duy nhất, dùng LRU (Least Recently Used) để quyết định dòng nào bị thay thế khi tập đầy.

conflict_miss_demo.js (verified bằng engine — Direct-Mapped vs 2-way)
// 2 dia chi CUNG index nhung KHAC tag, xen ke 10 lan (20 truy cap):
// addrA = 0, addrB = numSets * lineSize (trung index voi A)
const dm = makeDirectMappedCache(4, 4);   // Direct-Mapped: 20/20 MISS (100%!)
const sa = makeSetAssociativeCache(4, 2, 4); // 2-way: chi 2/20 MISS (10%)
// Set-Associative giu duoc CA HAI dia chi dong thoi trong CUNG mot set
⚠️ Cạm bẫy: Set-Associative không phải "miễn phí"
Tăng số "way" giảm Conflict Miss, nhưng mỗi lần truy cập phải so khớp tag với TẤT CẢ N dòng trong tập SONG SONG (mạch so sánh N-way) — tốn diện tích silicon và năng lượng hơn hẳn Direct-Mapped (chỉ so 1 tag). Đây là lý do L1 cache (cần cực nhanh, ít way — thường 4-8 way) khác hẳn L3 cache (chấp nhận chậm hơn để đổi lấy độ liên kết cao hơn, giảm miss — thường 16 way trở lên).

3. Tính toán chỉ số hiệu năng Cache: AMAT

Thời gian truy cập bộ nhớ TRUNG BÌNH (Average Memory Access Time) với 1 cấp cache:

$$AMAT = T_{Hit} + \text{MissRate} \times T_{MissPenalty}$$

Với hệ 2 cấp L1+L2, công thức mở rộng — missRateL2Local là tỷ lệ miss CỤC BỘ của L2 (chỉ tính trên số lần L1 ĐÃ miss, không phải trên tổng số truy cập chương trình):

$$AMAT = T_{HitL1} + \text{MissRateL1} \times (T_{HitL2} + \text{MissRateL2}_{local} \times T_{MissPenalty\_DRAM})$$

Verified thật (ví dụ kinh điển Patterson & Hennessy): $T_{HitL1}=1$, MissRateL1=2%, $T_{HitL2}=10$, MissRateL2 CỤC BỘ=25%, phạt truy cập DRAM=200 chu kỳ → $AMAT = 1 + 0,02 \times (10 + 0,25 \times 200) = \mathbf{2,2}$ chu kỳ.

amat.js (trích engine dùng chung cpu-core.js)
function amat(hitTime, missRate, missPenalty) {
  return hitTime + missRate * missPenalty;
}
function amatTwoLevel(hitTimeL1, missRateL1, hitTimeL2, missRateL2Local, missPenaltyMem) {
  return hitTimeL1 + missRateL1 * (hitTimeL2 + missRateL2Local * missPenaltyMem);
}
// Verified: amat(1, 0.05, 100) = 6
// Verified: amatTwoLevel(1, 0.02, 10, 0.25, 200) = 2.2 (vi du kinh dien P&H)
⚠️ Miss Rate cục bộ (Local) ≠ Miss Rate toàn cục (Global)
MissRateL2 CỤC BỘ (25% ở ví dụ trên) chỉ đo trên số lần L1 ĐÃ MISS — không phải trên TỔNG số truy cập chương trình. Miss Rate TOÀN CỤC của L2 (tỷ lệ truy cập chương trình phải chạm tới DRAM) = MissRateL1 × MissRateL2Local = $0,02 \times 0,25 = 0,005$ (0,5%) — một con số HOÀN TOÀN khác 25%. Nhầm lẫn 2 tỷ lệ này khi báo cáo hiệu năng hệ thống nhớ nhiều cấp là lỗi cực kỳ phổ biến.
locality_demo.js (verified bằng engine — duyệt hàng vs duyệt cột)
// Mang 8x8 phan tu 4-byte, cache 4 dong x 16 byte/dong (64 byte tong)
const rowMajorAddrs = []; // duyet: for row { for col { addrOf(row,col) } }
const colMajorAddrs = []; // duyet: for col { for row { addrOf(row,col) } }

runCacheTrace(makeDirectMappedCache(4, 4), rowMajorAddrs); // 16/64 miss (25%)
runCacheTrace(makeDirectMappedCache(4, 4), colMajorAddrs); // 64/64 miss (100%!)
// CUNG so phan tu duoc truy cap - chi khac THU TU - nhung miss rate gap 4 lan

4. Thực hành: Cache L1 Simulator

Bộ tách địa chỉ bên dưới tính trực tiếp Tag/Index/Offset từ một địa chỉ hex tuỳ chỉnh. Bộ mô phỏng Hit/ Miss chạy 4 kịch bản đã verify ở trên — đổi giữa Direct-Mapped và Set-Associative để tự tay thấy Conflict Miss biến mất:

🗄️ Bộ tách địa chỉ & Mô phỏng Cache Hit/Miss

Bộ tách Tag/Index/Offset

Mô phỏng Hit/Miss

Tóm lược

  • ✅ Cache che giấu Memory Wall bằng cách khai thác Temporal & Spatial Locality — không làm DRAM nhanh hơn, mà giảm số lần cần CHẠM tới DRAM.
  • ✅ Verified: duyệt mảng theo HÀNG (locality tốt) chỉ 25% miss; theo CỘT (locality mất) MISS 100% — gấp 4 lần tệ hơn dù cùng số phần tử.
  • ✅ Direct-Mapped đơn giản nhưng dễ Conflict Miss (verified 100% miss với 2 địa chỉ trùng index); 2-way Set-Associative giải quyết gần như hoàn toàn (verified chỉ 10% miss).
  • ✅ Verified: $AMAT$ 2 cấp (2% L1 miss, L2 hit=10, 25% L2 cục bộ, phạt DRAM=200) = 2,2 chu kỳ.
  • ✅ Pitfall: Miss Rate cục bộ (25%) và toàn cục (0,5%) là 2 con số khác hẳn nhau — đừng nhầm lẫn.

Trắc nghiệm ôn tập

Câu 1

Verified: duyệt mảng 8×8 theo HÀNG chỉ 25% miss, theo CỘT MISS 100%. Vì sao chênh lệch lớn vậy dù cùng số phần tử được truy cập?

Câu 2

Verified: 2 địa chỉ trùng index nhưng khác tag, xen kẽ 20 lần — Direct-Mapped MISS 100% (20/20), Set-Associative 2-way chỉ MISS 10% (2/20). Vì sao?

Câu 3

Verified: $AMAT = 1 + 0,02 \times (10 + 0,25 \times 200) = 2,2$ chu kỳ. Tỷ lệ 25% trong công thức là MissRateL2 CỤC BỘ. Miss Rate TOÀN CỤC của L2 (trên tổng số truy cập chương trình) là bao nhiêu?

Câu 4

Vì sao L1 cache thường dùng ít "way" hơn (vd 4-8 way) trong khi L3 cache có thể dùng 16 way trở lên?

Tải file code thực hành minh họa bài học

File JavaScript CPUJS — thư viện kiến trúc máy tính mini dùng xuyên suốt cả 12 bài, Bài 7 vừa thêm splitAddress(), makeDirectMappedCache(), makeSetAssociativeCache(), amat(), amatTwoLevel() — tách địa chỉ, mô phỏng cache Direct-Mapped/Set-Associative (LRU), và công thức AMAT nhiều cấp, kèm self-test đối chiếu đúng mọi con số trong bài (chạy node cpu-core.js, không cần cài thêm gì):

Tải về cpu-core.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 6: Song Song Cấp Lệnh & Thực Thi Ngoài Thứ Tự (Tomasulo) Bài 8: Bộ Nhớ Ảo & Khối TLB Quay lại Lộ trình Kiến Trúc Máy Tính