Tìm hiểu cách làm việc của bộ nhớ Cache (Phần 1)
Tìm hiểu cách làm việc của bộ nhớ Cache (Phần 2)
Cách làm việc
Khối tìm nạp của CPU sẽ tìm kiếm chỉ lệnh kế tiếp để được thực thi trong Cache chỉ lệnh L1. Nếu không có ở đó thì nó sẽ tìm kiếm trên Cache L2. Sau khi đó nếu cũng không có thì nó sẽ phải truy cập vào bộ nhớ RAM để nạp chỉ lệnh.
Chúng tôi gọi là một “hit” khi CPU nạp một chỉ lệnh đã được yêu cầu hoặc dữ liệu từ Cache, và gọi là một “miss” nếu chỉ lệnh hoặc dữ liệu được yêu cầu không có ở đó và CPU cần phải truy cập trực tiếp vào bộ nhớ RAM để lấy dữ liệu này.
Rõ ràng khi bạn mới bật máy tính thì Cache là hoàn toàn trống rỗng, vì vậy hệ thống sẽ phải truy cập vào bộ nhớ RAM – đây là một miss đối với Cache không thể tránh. Tuy nhiên sau khi chỉ lệnh đầu tiên được nạp, thì quá trình này sẽ bắt đầu.
Khi CPU nạp một chỉ lệnh từ một vị trí nhớ nào đó thì mạch đã gọi bộ điều khiển Cache nhớ sẽ nạp vào trong Cache nhớ một khối dữ liệu nhỏ bên dưới vị trí hiện hành mà CPU vừa mới nạp. Do các chương trình thường được thực hiện theo cách tuần tự nên vị trí nhớ tiếp theo mà CPU sẽ yêu cầu có thể là vị trí ngay bên dưới vị trí nhớ mà nó vừa nạp. Cũng do bộ điều khiển Cache nhớ đã nạp một số dữ liệu bên dưới vị trí đầu tiên được đọc bởi CPU rồi nên dữ liệu kế tiếp sẽ có thể nằm ở bên trong Cache nhớ, chính vì vậy CPU không cần phải truy cập vào RAM để lấy dữ liệu trong đó: nó đã được nạp vào bên trong Cache nhớ nhúng trong CPU, điều này làm cho nó có thể truy cập với tốc độ clock bên trong.
Lượng dữ liệu này được gọi là dòng và nó thường có chiều dài 64 byte.
Bên cạnh việc nạp một số lượng nhỏ dữ liệu này, bộ điều khiển nhớ cũng luôn tìm cách đoán xem những gì CPU sẽ yêu cầu tiếp theo. Một mạch có tên gọi là mạch tìm nạp trước, sẽ nạp nhiều dữ liệu được đặt sau 64 byte đầu tiên hơn từ RAM vào Cache nhớ. Nếu chương trình tiếp tục nạp chỉ lệnh và dữ liệu từ các vị trí nhớ theo cách tuần tự như vậy thì các chỉ lệnh và dữ liệu mà CPU sẽ hỏi tiếp theo đã được nạp vào trong Cache nhớ từ trước rồi.
Chúng ta có thể tóm tắt cách Cache nhớ làm việc như sau:
1. CPU yêu cầu chỉ lệnh hoặc dữ liệu đã được lưu tại địa chỉ “a”.
2. Do nội dung từ địa chỉ “a” không có bên trong Cache nhớ nên CPU phải tìm nạp nó trực tiếp từ RAM.
3. Bộ điều khiển Cache sẽ nạp một dòng (thường là 64 byte) bắt đầu từ địa chỉ “a” vào Cache nhớ. Nó sẽ nạp nhiều hơn dữ lượng dữ liệu mà CPU yêu cầu, chính vì vậy nếu chương trình tiếp tục chạy tuần tự (nghĩa là yêu cầu địa chỉ a +1) thì chỉ lệnh hoặc dữ liệu kế tiếp mà CPU sẽ hỏi đã được nạp trong Cache nhớ từ trước đó rồi.
4. Mạch có tên gọi là tìm nạp trước sẽ nạp nhiều dữ liệu được đặt sau dòng này, có nghĩa là bắt đầu việc nạp các nội dung từ địa chỉ a + 64 trở đi vào Cache. Để cho bạn một ví dụ thực tế là các CPU của Pentium 4 có bộ tìm nạp trước 256-byte, chính vì vậy nó có thể nạp được 256byte kế tiếp sau dòng dữ liệu đã được nạp vào trong Cache.
Nếu chương trình chạy một cách tuần tự thì CPU sẽ không cần phải tìm nạp dữ liệu bằng cách truy cập trực tiếp vào bộ nhớ RAM, ngoại trừ nạp mỗi chỉ lệnh đầu tiên – vì các chỉ lệnh và dữ liệu được yêu cầu bởi CPU sẽ luôn nằm bên trong Cache nhớ trước khi CPU yêu cầu đến chúng.
Mặc dù các chương trình không chạy luôn giống như vậy, đôi khi chúng có thể nhảy từ một vị trí nhớ này sang vị trí nhớ khác. Thách thức chính của bộ điều khiển Cache chính là việc đoán những địa chỉ gì mà CPU sẽ nhảy đến, và từ đó nạp những nội dung của địa chỉ này vào trong Cache nhớ trước khi CPU yêu cầu để tránh trường hợp CPU phải truy cập vào bộ nhớ RAM là giảm hiệu suất của hệ thống. Nhiệm vụ này được gọi là dự đoán rẽ nhánh và tất cả các CPU hiện đại đều có tính năng này.
Các CPU hiện đại có tốc độ hit ít nhất cũng là 80%, nghĩa là 80% của thời gian CPU không truy cập trực tiếp vào bộ nhớ RAM, mà thay vào đó là Cache nhớ.
Tổ chức Cache nhớ
Cache nhớ được chia thành các dòng bên trong, mỗi một dòng dữ từ 16 đến 128byte, phụ thuộc vào CPU. Đối với đại đa số các CPU hiện hành thì Cache nhớ được tổ chức theo các dòng 64byte (512bit), chính vì vậy chúng tôi sẽ xem xét đến Cache nhớ đang sử dụng dòng 64byte trong các ví dụ xuyên suốt từ đầu hướng dẫn này. Phần dưới chúng tôi sẽ trình bày các chi tiết kỹ thuật chính của Cache nhớ cho tất cả các CPU hiện đang có trên thị trường.
Cache nhớ 512 KB L2 được chia thành 8.192 dòng. Bạn nên lưu ý rằng 1KB là 2^10 hay 1.024 byte chứ không phải là 1.000byte, chính vì vậy 524.288 / 64 = 8.192. Chúng ta sẽ xem xét đến CPU một lõi có Cache nhớ 512 KB L2 trong các ví dụ. Trên hình 5 chúng tôi mô phỏng cách tổ chức bên trong của Cache nhớ này.
Hình 5: Cách tổ chức Cache nhớ L2 512 KB
Cache nhớ có thể làm việc dưới ba kiểu cấu hình khác nhau: bản đồ hóa trực tiếp, liên kết toàn bộ và tập liên kết (theo nhiều dòng).
Bản đồ hóa trực tiếp
Bản đồ hóa trực tiếp là cách đơn giản nhất để tạo một Cache nhớ. Trong cấu hình này, bộ nhớ RAM chính được chia thành các dòng bằng nhau nằm bên trong Cache nhớ. Nếu chúng ta có một hệ thống 1GB RAM thì 1GB này sẽ được chia thành 8.192 khối (giả dụ rằng Cache nhớ sử dụng cấu hình mà chúng ta đã mô tả ở trên), mỗi một khối có 128KB (1.073.741.824 / 8.192 = 131.072 – lưu ý rằng 1GB là 2^30 bytes, 1 MB là 2^20 byte và 1 KB sẽ là 2^10 byte). Nếu hệ thống của bạn có 512MB thì bộ nhớ cũng sẽ được chia thành 8.192 khối nhưng mỗi một khối này chỉ có 64 KB. Chúng tôi có minh chứng cách tổ chức này trong hình 6 bên dưới.
Hình 6: Cách bản đồ hóa trực tiếp các làm việc của Cache
Ưu điển của phương pháp bản đồ hóa trực tiếp là nó là cách đơn giản nhất.
Khi CPU yêu cầu một địa chỉ nào đó từ bộ nhớ RAM (ví dụ địa chỉ 1.000) thì bộ điều khiển Cache sẽ nạp một dòng (64byte) từ bộ nhớ RAM và lưu dòng này trên Cache nhớ (nghĩa là từ địa chỉ 1.000 đến 1.063, giả dụ rằng chúng ta đang sử dụng lược đồ địa chỉ 8 bit). Vì vậy nếu CPU lại yêu cầu các nội dung của địa chỉ này hoặc của một số địa chỉ tiếp theo sau đó (nghĩa là các địa chỉ từ 1.000 đến 1.063) thì chúng sẽ được nằm sẵn bên trong Cache.
Vấn đề ở đây là nếu CPU cần đến hai địa chỉ được bản đồ hóa đến cùng một dòng Cache giống nhau, thì lúc này một miss sẽ xuất hiện (vấn đề này được gọi là hiện tượng xung đột). Tiếp tục ví dụ của chúng ta, nếu CPU yêu cầu địa chỉ 1.000 và sau đó yêu cầu địa chỉ 2.000 thì một miss cũng sẽ xuất hiện vì hai địa chỉ này đều nằm trong cùng một khối 128KB, và những gì bên trong Cache là một dòng bắt đầu từ địa chỉ 1.000. Chính vì vậy bộ điều khiển Cache sẽ nạp một dòng từ địa chỉ 2.000 và lưu nó trên dòng đầu tiên của Cache nhớ, xóa các nội dung trước đó, trong trường hợp của chúng ta thì đó là dòng từ địa chỉ 1.000.
Cũng một vấn đề nữa. Nếu chương trình có một vòng lặp nhiều hơn 64 bytes thì lúc này cũng có một miss xuất nhiện trong toàn bộ khoảng thời gian của vòng lặp.
Ví dụ, nếu vòng lặp thực hiện từ địa chỉ 1.000 đến địa chỉ 1.100 thì CPU sẽ phải nạp tất cả các chỉ lệnh trực tiếp từ bộ nhớ RAM trong suốt khoảng thời gian của vòng lặp. Vấn đề này sẽ xảy ra vì Cache sẽ có nội dung từ các địa chỉ 1.000 đến 1.063 và khi CPU yêu cầu các nội dung từ địa chỉ 1.100 thì nó sẽ phải vào bộ nhớ RAM để lấy dữ liệu, và sau đó bộ điều khiển Cache sẽ nạp các địa chỉ từ 1.100 đến 1.163. Khi CPU yêu cầu lại địa chỉ 1.000 thì nó sẽ phải quay trở lại bộ nhớ RAM, vì lúc này Cache sẽ không có các thành phần dữ liệu từ địa chỉ 1.000. Nếu vòng lặp này được thực thi 1.000 lần thì CPU sẽ phải vào bộ nhớ RAM để nạp dữ liệu cũng 1.000 lần.
Đó chính là lý do tại sao việc bản đồ hóa trực tiếp Cache nhớ lại ít hiệu quả và ít được sử dụng nữa.
Sự liên kết toàn bộ
Cấu hình liên kết toàn bộ, hay nói theo cách khác là không có sự khó khăn trong việc liên kết giữa các dòng của Cache nhớ và vị trí của bộ nhớ RAM. Bộ điều khiển Cache có thể lưu bất kỳ địa chỉ nào. Như vậy các vấn đề đã được nói ở trên sẽ không xảy ra. Cấu hình này là cấu hình hiệu quả nhất (nghĩa là cấu hình có tốc độ hit cao nhất).
Nói theo cách khác, mạch điều khiển sẽ phức tạp hơn nhiều, vì nó cần phải giữ được việc kiểm tra xem các vị trí nhớ nào được nạp bên trong Cache nhớ. Điều này là lý do cho ra đời một giải pháp lai – có tên gọi là tập liên kết – được sử dụng rộng rãi ngày nay.