Bộ nhớ/Vì sao có cache — và những con số độ trễ nên nhớ
8/13
Bài 8 / 13~17 phútCache & memory hierarchyMiễn phí lượt xem

Vì sao có cache — và những con số độ trễ nên nhớ

CPU nhanh hơn RAM hàng trăm lần, nên có nhiều lớp bộ nhớ: thanh ghi, L1/L2/L3, RAM, SSD, mạng. Bài học thang độ trễ để ước lượng hiệu năng và biết bottleneck nằm đâu.

TL;DR: Tốc độ CPU và tốc độ bộ nhớ đã rời nhau hàng chục năm: CPU làm vài tỉ lệnh/giây, nhưng một lần đọc RAM tốn ~100 nanosecond — tương đương hàng trăm lệnh bị bỏ phí nếu CPU ngồi chờ. Giải pháp là memory hierarchy: xếp tầng nhiều loại bộ nhớ, từ nhỏ-và-cực-nhanh (thanh ghi, cache L1) tới lớn-và-chậm (RAM, SSD, mạng). Dữ liệu hay dùng được giữ ở tầng nhanh; tầng càng xa CPU càng lớn nhưng càng chậm theo bậc. Mỗi lập trình viên nên thuộc thang độ trễ tương đối giữa các tầng — nó cho bạn ước lượng nhanh "thao tác này tốn gì" và biết bottleneck nằm ở đâu trước cả khi đo.

Bạn viết một vòng lặp O(n) đẹp đẽ nhưng nó chậm bất ngờ. Bạn thay một HashMap bằng cấu trúc "tối ưu hơn về big-O" nhưng thực tế lại chậm hơn. Lý do thường không phải số phép tính, mà là mỗi phép tính phải chờ dữ liệu từ tầng bộ nhớ nào. Một lần đọc trúng cache L1 và một lần đọc trượt xuống RAM chênh nhau cả trăm lần — và big-O không thấy điều đó.

Bài này giải thích vì sao tồn tại nhiều lớp bộ nhớ, đặt ra thang độ trễ để bạn ghi nhớ, và cho bạn cách dùng nó để ước lượng hiệu năng.

1. Analogy — bàn làm việc, ngăn kéo, kho lưu trữ

Hình dung bạn đang làm một dự án giấy tờ:

  • Vài tờ đang cầm trên tay — lấy tức thì (thanh ghi).
  • Tập hồ sơ trên mặt bàn — với tay là tới (cache L1/L2).
  • Tài liệu trong ngăn kéo bàn — mở ngăn, mất vài giây (cache L3).
  • Hồ sơ trong tủ tài liệu cuối phòng — đứng dậy đi lấy (RAM).
  • Thùng lưu trữ dưới tầng hầm — đi thang máy xuống (SSD).
  • Tài liệu phải xin từ chi nhánh thành phố khác — gửi yêu cầu, chờ chuyển (mạng).

Bạn không để mọi thứ trên tay (không đủ chỗ) và không để mọi thứ dưới hầm (quá chậm). Bạn giữ thứ đang dùng gần nhất, đẩy thứ ít dùng ra xa. CPU làm y hệt với dữ liệu.

Bàn làm việcBộ nhớ máy tínhĐộ trễ tương đối
Giấy trên tayThanh ghi (register)~1
Hồ sơ trên mặt bànCache L1~vài lần
Ngăn kéo bànCache L2 / L3~chục lần
Tủ cuối phòngRAM~trăm lần
Thùng dưới hầmSSD~chục nghìn lần
Chi nhánh thành phố khácMạng (cùng vùng)~triệu lần
💡 Cách nhớ

Mỗi tầng đi xa CPU một bước thì chậm hơn khoảng một bậc độ lớn (10 lần) nhưng lớn hơn nhiều. Bộ nhớ nhanh thì đắt và nhỏ; bộ nhớ rẻ thì chậm và lớn. Hierarchy tồn tại để có cả hai: ảo giác "lớn và nhanh" bằng cách giữ phần nóng ở gần.

2. Cơ chế — vì sao không làm RAM nhanh bằng cache?

Câu hỏi tự nhiên: nếu cache nhanh thế, sao không làm toàn bộ RAM bằng vật liệu cache? Hai lý do vật lý và kinh tế:

  • Công nghệ khác nhau. Cache dùng SRAM (6 transistor/bit, nhanh, không cần refresh nhưng đắt và tốn diện tích). RAM dùng DRAM (1 transistor + 1 tụ/bit, rẻ và đặc, nhưng chậm hơn và phải refresh liên tục). Làm 16 GB toàn SRAM sẽ cực kỳ đắt và to.
  • Tốc độ ánh sáng và khoảng cách. Tín hiệu điện đi trong dây với tốc độ hữu hạn. Cache L1 nằm ngay trong nhân CPU, cách ALU vài milimet; RAM nằm trên thanh cách CPU vài centimet. Khoảng cách đó, nhân với số lần truy cập, tạo độ trễ thật.

Đây là dãy tầng điển hình trên một CPU desktop hiện đại, từ gần CPU ra xa:

CPU
CPU + thanh ghi
Cache
L1 / L2 / L3
RAM
RAM
SSD
SSD / đĩa
Network
Mạng

Khi CPU cần một dữ liệu, nó hỏi tầng gần nhất trước (L1). Trúng (hit) → trả về ngay. Trượt (miss) → hỏi xuống L2, L3, rồi RAM, dừng ở tầng nào có dữ liệu. Mỗi lần trượt xuống một tầng cộng thêm độ trễ của tầng đó.

flowchart TD
  CPU["CPU"] --> L1{"L1 hit?"}
  L1 -->|hit ~1ns| DONE["Tra ve du lieu"]
  L1 -->|miss| L2{"L2 hit?"}
  L2 -->|hit ~4ns| DONE
  L2 -->|miss| L3{"L3 hit?"}
  L3 -->|hit ~12ns| DONE
  L3 -->|miss| RAM["RAM ~100ns"]
  RAM --> DONE

3. Thang độ trễ nên nhớ

Đây là phiên bản gọn của "latency numbers every programmer should know" (Jeff Dean), làm tròn cho dễ nhớ. Con số tuyệt đối thay đổi theo phần cứng — điều quan trọng là tỉ lệ giữa các tầng.

Thao tácĐộ trễ xấp xỉSo với L1
Truy cập thanh ghidưới 1 ns
Cache L1~1 ns
Cache L2~4 ns~4×
Cache L3~12 ns~12×
RAM (DRAM)~100 ns~100×
SSD đọc ngẫu nhiên~16.000 ns (16 µs)~16.000×
Đĩa quay (HDD) seek~2.000.000 ns (2 ms)~2 triệu×
Mạng trong cùng datacenter~500.000 ns (0.5 ms)~500.000×
Mạng qua lục địa (round trip)~150.000.000 ns (150 ms)~150 triệu×

Một phép so sánh trực giác (thu nhỏ về "thời gian con người"): nếu một lần truy cập L1 mất 1 giây, thì đọc RAM mất gần 2 phút, đọc SSD mất gần 4 tiếng rưỡi, và một round trip mạng qua lục địa mất gần 5 năm.

Vì sao thang này quan trọng hơn big-O đôi khi

Big-O đếm số thao tác, coi mọi thao tác có chi phí như nhau. Nhưng một thuật toán O(n) mà mỗi bước trượt cache xuống RAM (~100 ns) có thể chậm hơn một thuật toán O(n log n) mà mọi bước trúng L1 (~1 ns). Với n vừa phải, hằng số ẩn (do tầng bộ nhớ) thắng cả độ phức tạp tiệm cận. Đây là lý do "thuật toán tốt hơn trên giấy" đôi khi thua trên máy thật — và vì sao phần còn lại của module dạy bạn giữ dữ liệu ở tầng nhanh.

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

Thang độ trễ là công cụ ước lượng. Vài cách dùng nó hằng ngày:

Ước lượng "back-of-envelope" trước khi tối ưu. Một vòng lặp duyệt 1 triệu phần tử, mỗi phần tử buộc một lần đọc RAM (~100 ns) → tối thiểu ~100 ms chỉ riêng chờ bộ nhớ. Nếu giữ được trong cache (~1–10 ns/phần tử) → ~1–10 ms. Con số này cho bạn biết "có đáng tối ưu locality không" trước khi viết một dòng benchmark.

Nhận ra ranh giới tầng là nơi hiệu năng nhảy bậc. Khi dữ liệu vượt kích thước một tầng cache, độ trễ trung bình tăng vọt. Đây là lý do một thuật toán "đột nhiên chậm" khi n vượt một ngưỡng — bạn vừa tràn khỏi L2 hoặc L3:

Kich thuoc du lieu nho hon L1 (~32KB)   -> moi truy cap ~1ns
Vuot L1 nhung trong L2 (~256KB-1MB)     -> ~4ns
Vuot L3 (~8-32MB)                        -> ~100ns (xuong RAM)

Thiết kế để giảm lần "đi xa". Mỗi cú nhảy xuống tầng chậm là một cơ hội tối ưu: gộp nhiều request mạng thành một (giảm round trip), đọc tuần tự từ đĩa thay vì ngẫu nhiên, giữ dữ liệu nóng trong RAM thay vì query DB lặp lại. Cùng một nguyên lý ở mọi tầng: đừng đi xa nếu có thể lấy gần.

Đừng tối ưu tầng sai. Nếu bottleneck là một round trip mạng 150 ms, tối ưu cache L1 (tiết kiệm vài ns) là vô nghĩa. Thang độ trễ giúp bạn nhắm đúng tầng đắt nhất trong đường đi nóng (hot path) — quy tắc này sẽ trở lại ở capstone và ở Course 4 (I/O).

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

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

Kích thước cache thực tế (desktop 2020s): L1 thường 32 KB data + 32 KB instruction mỗi nhân, độ trễ ~4 chu kỳ (tương đương ~1 ns ở tần số ~4 GHz — khớp với bảng độ trễ ở mục 3). L2 256 KB–1 MB mỗi nhân, ~12 chu kỳ. L3 8–32 MB dùng chung giữa các nhân, ~40 chu kỳ. Con số chu kỳ ổn định hơn nanosecond vì độc lập với tần số clock.

Bandwidth vs latency: thang trên là độ trễ (thời gian một lần truy cập). Còn bandwidth (lượng dữ liệu/giây) là chỉ số khác: RAM có bandwidth hàng chục GB/s dù latency ~100 ns, vì nó truyền nhiều dữ liệu song song sau khi đã "khởi động". Đây là lý do đọc tuần tự (streaming) một mảng lớn nhanh hơn nhiều so với latency gợi ý — prefetcher và bandwidth bù lại. Bài 02 (cache line) giải thích cơ chế.

Bức tường bộ nhớ (memory wall): từ thập niên 1990, tốc độ CPU tăng nhanh hơn tốc độ RAM rất nhiều, khoảng cách ngày càng giãn. Hệ quả: với phần lớn phần mềm hiện đại, bottleneck không còn là "CPU tính chậm" mà là "CPU chờ bộ nhớ". Đây là nền tảng triết lý của cả module: tối ưu bộ nhớ thường lời hơn tối ưu phép tính.

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

7. Tóm tắt

  • CPU nhanh hơn RAM hàng trăm lần; nếu chờ RAM cho mỗi truy cập, CPU sẽ nghỉ phần lớn thời gian.
  • Memory hierarchy xếp tầng bộ nhớ: gần CPU thì nhỏ-nhanh-đắt (thanh ghi, L1), xa CPU thì lớn-chậm-rẻ (RAM, SSD, mạng).
  • Không làm toàn bộ RAM bằng cache được vì SRAM đắt và to, và vì tốc độ tín hiệu giới hạn bởi khoảng cách vật lý.
  • Thang độ trễ nên nhớ (tương đối với L1): L2 ~4×, L3 ~12×, RAM ~100×, SSD ~16.000×, mạng qua lục địa ~150 triệu×.
  • Big-O đếm số thao tác nhưng bỏ qua tầng bộ nhớ; một thuật toán "tốt hơn trên giấy" có thể thua nếu nó trượt cache nhiều.
  • Dùng thang này để ước lượng nhanh, nhận ra ranh giới tầng (nơi hiệu năng nhảy bậc), và nhắm đúng tầng đắt nhất trong hot path.

8. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao máy tính cần nhiều lớp bộ nhớ thay vì chỉ một loại RAM lớn và nhanh?
Vì không có loại bộ nhớ nào vừa nhanh, vừa lớn, vừa rẻ cùng lúc. Bộ nhớ nhanh (SRAM dùng cho cache) đắt và tốn diện tích (~6 transistor/bit), nên chỉ làm được dung lượng nhỏ; bộ nhớ rẻ và đặc (DRAM dùng cho RAM) thì chậm hơn và phải refresh liên tục. Thêm vào đó, khoảng cách vật lý giới hạn tốc độ: cache L1 nằm ngay trong nhân CPU nên truy cập cực nhanh, còn RAM cách vài centimet nên chậm hơn. Hierarchy giải quyết bằng cách giữ dữ liệu nóng ở tầng nhanh và đẩy dữ liệu nguội ra tầng lớn-chậm, tạo ảo giác "vừa lớn vừa nhanh" mà không phải trả giá làm toàn bộ bằng vật liệu đắt.
Q2
Đọc một byte từ cache L1 nhanh hơn đọc từ RAM khoảng bao nhiêu lần? Hệ quả thực tế của con số đó là gì?
Khoảng 100 lần (L1 ~1 ns, RAM ~100 ns). Hệ quả thực tế: một vòng lặp mà mọi truy cập trúng L1 có thể nhanh hơn cùng vòng lặp đó nhưng trượt xuống RAM tới hai bậc độ lớn — dù số phép tính y hệt. Đây là lý do hai đoạn code cùng big-O, cùng số phép tính, có thể chênh nhau nhiều lần về tốc độ chỉ vì một bên giữ được dữ liệu trong cache còn bên kia liên tục trượt xuống RAM. Tối ưu để dữ liệu nóng nằm trong cache thường lời hơn tối ưu bản thân thuật toán, một khi big-O đã hợp lý.
Q3
Một thuật toán O(n) có thể chậm hơn một thuật toán O(n log n) trên cùng dữ liệu. Giải thích bằng thang độ trễ vì sao điều này xảy ra.
Big-O đếm số thao tác và ngầm giả định mọi thao tác có chi phí như nhau — nhưng thực tế chi phí một thao tác phụ thuộc nó chạm tầng bộ nhớ nào. Nếu thuật toán O(n) truy cập bộ nhớ ngẫu nhiên, mỗi bước có thể trượt cache xuống RAM (~100 ns); còn thuật toán O(n log n) truy cập tuần tự, trúng cache (~1 ns/bước), thì với n vừa phải, n × 100ns có thể lớn hơn n log n × 1ns. Hằng số ẩn do tầng bộ nhớ (gấp ~100 lần) lấn át lợi thế tiệm cận của big-O. Đây là lý do phải đo trên dữ liệu thật, không chỉ tin phân tích tiệm cận.
Q4
Vì sao một thuật toán có thể 'đột nhiên chậm hẳn' khi kích thước dữ liệu vượt một ngưỡng nào đó?
Vì dữ liệu vừa tràn khỏi một tầng cache. Khi toàn bộ dữ liệu vừa trong L1 (~32 KB), mọi truy cập ~1 ns. Khi dữ liệu lớn hơn L1 nhưng còn trong L2/L3, độ trễ trung bình tăng lên ~4–12 ns. Khi vượt cả L3 (~8–32 MB), phần lớn truy cập phải xuống RAM (~100 ns). Mỗi lần vượt một ranh giới tầng, độ trễ trung bình nhảy bậc, nên đường cong thời gian-theo-kích-thước có các "bậc thang" rõ rệt thay vì tuyến tính mượt. Quan sát ngưỡng nơi thuật toán chậm hẳn thường cho biết kích thước tầng cache của máy.
Q5
Nếu bottleneck của một hàm là một round trip mạng qua lục địa (~150 ms), vì sao tối ưu để dữ liệu nằm trong cache L1 là vô nghĩa?
Vì hai chi phí lệch nhau quá nhiều bậc. Một round trip mạng qua lục địa ~150 ms = 150.000.000 ns; một truy cập cache L1 ~1 ns. Tối ưu cache tiết kiệm vài ns trên tổng chi phí 150 triệu ns — phần tiết kiệm nhỏ tới mức không đo nổi. Quy tắc rút ra: luôn nhắm tầng đắt nhất trong đường đi nóng. Thang độ trễ giúp bạn xếp hạng chi phí và tập trung vào cú nhảy xuống tầng chậm nhất (ở đây là mạng) — ví dụ gộp nhiều request thành một, cache kết quả phía client — thay vì tối ưu tầng vốn đã rẻ. Tối ưu sai tầng là lãng phí công sức phổ biến.
Q6
Độ trễ (latency) và bandwidth khác nhau thế nào? Vì sao đọc tuần tự một mảng lớn nhanh hơn con số latency RAM gợi ý?
Latency là thời gian chờ cho một lần truy cập (RAM ~100 ns). Bandwidth là lượng dữ liệu chuyển được mỗi giây khi đã truyền liên tục (RAM hàng chục GB/s). Chúng độc lập: một kênh có thể latency cao nhưng bandwidth lớn. Khi đọc tuần tự một mảng lớn, bạn không trả latency 100 ns cho từng byte — sau lần khởi động đầu, dữ liệu chảy về theo từng cache line 64 byte, và prefetcher đoán trước, nạp sẵn các line kế tiếp khi CPU còn đang xử lý line hiện tại. Nhờ đó truy cập tuần tự đạt gần bandwidth tối đa, nhanh hơn nhiều so với con số latency cho từng truy cập ngẫu nhiên gợi ý. Đây chính là động lực cho "duyệt tuần tự" ở các bài sau.

Bài tiếp theo: Cache line và locality

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