Giao thức nhất quán cache (Cache Coherence, MESI)
1. Tổng quan
A. Định nghĩa
Nhất quán cache (Cache Coherence) là tính chất bảo đảm rằng ngay cả khi nhiều bộ xử lý (lõi) cùng giữ bản sao của cùng một khối bộ nhớ trong cache riêng (private) của mình, thì bất kỳ lõi nào đọc khối đó cũng quan sát được giá trị duy nhất được ghi gần nhất; máy trạng thái (state machine) cưỡng chế điều này bằng phần cứng chính là giao thức nhất quán cache (cache coherence protocol). Hiện thực tiêu biểu là giao thức MESI, quản lý mỗi dòng cache ở một trong bốn trạng thái: Modified, Exclusive, Shared, Invalid.
Bản chất của nhất quán cache là "che giấu mâu thuẫn phát sinh khi sao chép dữ liệu ở nhiều nơi vì mục đích hiệu năng". Trong hệ đa lõi, mỗi lõi giữ cache L1/L2 riêng để giảm độ trễ truy cập bộ nhớ. Nhưng nếu hai lõi đã nạp cùng biến x vào cache của mình mà một lõi cập nhật x, thì cache của lõi kia còn lại giá trị cũ (stale). Không có giao thức thì sinh ra mâu thuẫn "lõi B vẫn đọc x bằng 0 dù lõi A đã đổi nó thành 1". Giao thức nhất quán cache vô hiệu hóa (invalidate) hoặc cập nhật (update) các bản sao khác ngay thời điểm ghi, giúp phần mềm duy trì ảo giác "một bộ nhớ chia sẻ duy nhất không có cache".
Vì sao lại đúng bốn trạng thái cũng được giải thích bằng logic thiết kế. Thuộc tính cốt lõi cần phân biệt cho một dòng cache gồm ba điều: "có hợp lệ không (valid)", "chỉ mình ta giữ không (exclusive)", "có giống bộ nhớ không (clean)". Nếu không hợp lệ là Invalid; nếu hợp lệ, độc nhất và clean là Exclusive; nếu hợp lệ, độc nhất nhưng dirty là Modified; và nếu hợp lệ nhưng chia sẻ (clean) với cache khác là Shared — bốn tổ hợp phát sinh tự nhiên. MSI gọn hơn không biểu diễn riêng được "độc nhất clean" nên gây ra vô hiệu hóa không cần thiết, còn MOESI/MESIF giàu hơn thêm trạng thái để biểu diễn thuộc tính bổ sung ("bên chịu trách nhiệm cung cấp khối dirty đang chia sẻ" hoặc "bên đáp ứng được chỉ định"). Nói ngắn gọn, kích thước tập trạng thái là kết quả của phán đoán chi phí-lợi ích về "theo dõi đến mức phân biệt nào bằng phần cứng để tiết kiệm lưu lượng".
Ở đây cần phân biệt khái niệm nhất quán (coherence) và tính nhất quán/mô hình nhất quán bộ nhớ (consistency). Coherence là tính chất cục bộ xử lý thứ tự và khả năng thấy của các lần ghi vào "một địa chỉ đơn", còn consistency là quy tắc toàn cục (ví dụ Sequential Consistency, TSO) quy định các truy cập vào "nhiều địa chỉ khác nhau" được quan sát theo thứ tự nào. Tức MESI bảo đảm coherence, nhưng vấn đề sắp xếp lại (reordering) giữa các biến khác nhau vẫn thuộc lĩnh vực của rào chắn bộ nhớ (memory barrier) và mô hình consistency. Trong bài làm của Kỹ sư chuyên nghiệp, không nhầm lẫn hai điều này là cốt yếu.
B. Bối cảnh ra đời và sự cần thiết
Thời đơn lõi chỉ có một cache nên chỉ cần quản lý vấn đề duy nhất "giá trị cache và giá trị bộ nhớ có thể khác nhau" (sự bất nhất do write-back) là đủ. Nhưng từ giữa những năm 2000, khi việc nâng xung nhịp va vào bức tường điện năng và nhiệt (Power Wall), trục mở rộng hiệu năng chuyển từ "một lõi nhanh hơn" sang "nhiều lõi", và mọi máy chủ, PC, điện thoại thông minh đều trở thành đa lõi. Khi việc đặt cache riêng cho từng lõi trở nên phổ biến, việc bản sao của cùng dữ liệu rải rác ở nhiều cache trở thành thường trực, và một cơ chế phần cứng quản lý điều đó không mâu thuẫn trở thành thiết yếu.
Nhất quán cache cũng gắn với chính sách ghi của cache (write-back/write-through). Phần lớn cache hiện đại dùng write-back, không phản ánh lần ghi vào bộ nhớ ngay mà chỉ ghi vào cache rồi viết lại sau; cách này giảm mạnh lưu lượng bộ nhớ nhưng tạo ra trạng thái "chỉ cache có giá trị mới nhất" (M trong MESI). Do đó giao thức nhất quán phải điều phối sao cho khi một lõi yêu cầu khối đó, cache giữ giá trị mới nhất đáp ứng chứ không phải bộ nhớ. Với write-through, bộ nhớ luôn là mới nhất nên việc điều phối này đơn giản nhưng lại tiêu quá mức băng thông bộ nhớ; quản lý nhất quán trở nên phức tạp như cái giá của việc chọn write-back vì hiệu năng.
Sự cần thiết này gắn trực tiếp với hiệu năng thực tế. Chẳng hạn trong chương trình đa luồng, khi các luồng cập nhật một bộ đếm chia sẻ hoặc tranh nhau một biến khóa (lock), dòng cache đó liên tục qua lại giữa các lõi — ping-pong dòng cache — và độ trễ hàng chục đến hàng trăm chu kỳ lặp lại. Giao thức nhất quán vừa là cơ chế an toàn bảo đảm tính đúng đắn, vừa là yếu tố hiệu năng mà chi phí lưu lượng của nó quyết định trần của khả năng mở rộng song song. Do đó kỹ sư thiết kế máy chủ hiệu năng cao, DBMS in-memory, cấu trúc dữ liệu lock-free, nhân HPC phải hiểu nguyên lý hoạt động của giao thức và bố trí dữ liệu để tránh chia sẻ giả (false sharing).
2. Cấu trúc của vấn đề nhất quán cache và các yêu cầu
Phân tầng bộ nhớ đa lõi, như dưới đây, gồm "cache riêng theo từng lõi + LLC (Last-Level Cache) chia sẻ + bộ nhớ chính", và giao thức nhất quán hoạt động ở tầng điều phối các bản sao giữa các cache riêng.
flowchart TB
subgraph Chip["Bộ xử lý đa lõi"]
C0["Lõi 0"] --> L10["Cache riêng L1/L2"]
C1["Lõi 1"] --> L11["Cache riêng L1/L2"]
C2["Lõi 2"] --> L12["Cache riêng L1/L2"]
L10 --- BUS["Liên kết nhất quán<br/>(bus chung / ring / mesh)"]
L11 --- BUS
L12 --- BUS
BUS --- LLC["L3(LLC) chia sẻ<br/>+ snoop filter/directory"]
end
LLC --- MEM[("Bộ nhớ chính(DRAM)")]
style Chip fill:#e8f0fe,stroke:#2f6fed,stroke-width:2px
style LLC fill:#fef7e0,stroke:#f9ab00,stroke-width:2px
Trong hình trên, vị trí của snoop filter/directory vẽ cùng L3 (LLC) chia sẻ là quan trọng. Nhiều CPU thương mại thiết kế LLC dạng bao hàm (inclusive), sao cho dòng nào ở cache riêng tầng trên cũng phải tồn tại trong LLC. Khi đó mỗi dòng LLC được gắn bit hiện diện (presence bit) — chỉ "lõi nào giữ dòng này trong cache riêng" — nên trên thực tế LLC kiêm luôn vai trò directory và snoop filter trên chip. Nhờ vậy, snoop đến từ bên ngoài hay yêu cầu giữa các lõi có thể chỉ được chuyển tới lõi đang giữ dòng thay vì phát quảng bá tới mọi lõi. Tuy nhiên dạng bao hàm có chi phí vô hiệu hóa ngược (back-invalidation) — khi một dòng bị đẩy khỏi LLC, bản sao ở cache tầng trên cũng phải bị vô hiệu hóa theo — nên gần đây các thiết kế kết hợp dạng không bao hàm (non-inclusive) với snoop filter riêng ngày càng nhiều.
Điều kiện mà giao thức nhất quán phải thỏa thường được tóm thành ba. Thứ nhất là lan truyền ghi (write propagation), nghĩa là kết quả ghi của một lõi rốt cuộc phải được phản ánh vào lần đọc của lõi khác. Thứ hai là tuần tự hóa ghi (write serialization), nghĩa là mọi lần ghi vào cùng địa chỉ phải hiện ra với mọi lõi theo cùng một thứ tự. Nếu lõi A đã ghi x=1 rồi x=2 theo thứ tự, thì không lõi nào khác được trải nghiệm sự đảo ngược là thấy 2 rồi mới thấy 1. Thứ ba là trả về giá trị mới nhất, nghĩa là lần đọc phải trả về giá trị của "lần ghi hoàn tất cuối cùng".
Lấy ví dụ cụ thể, xét hai lõi xử lý biến chia sẻ flag (giá trị đầu 0). Để lõi A ghi flag=1, trước tiên nó phải sở hữu dòng đó ở trạng thái ghi được. Nếu lõi B đang giữ cùng dòng để đọc, yêu cầu ghi của A sẽ vô hiệu hóa bản sao của B; về sau khi B đọc lại flag, xảy ra cache miss và nó lấy về giá trị mới nhất là 1 từ A (hoặc bộ nhớ). Như vậy ba nhịp "ghi → vô hiệu hóa bản sao khác → nạp lại" đạt được lan truyền ghi và tuần tự hóa cùng lúc. Điều quan trọng là toàn bộ quá trình này vô hình với phần mềm; lập trình viên chỉ đơn thuần đọc và ghi bộ nhớ. Vì phần cứng chịu trách nhiệm về tính đúng đắn nên lập trình đa lõi mới khả thi, nhưng sự thật rằng lưu lượng vô hiệu hóa phía sau đó bị chuyển thành chi phí hiệu năng là điều kỹ sư hiệu năng phải luôn ý thức.
Ranh giới giữa coherence và consistency trở nên rõ hơn qua một ví dụ sắp xếp lại cụ thể. Xét mẫu cờ điển hình: lõi A ghi data=42 rồi ghi ready=1, còn lõi B kiểm ready==1 rồi đọc data. MESI bảo đảm "một giá trị mới nhất duy nhất" cho data và ready mỗi cái, nhưng nó không bảo đảm rằng các lần ghi của A vào hai biến hiện ra với B theo cùng thứ tự. Dưới mô hình bộ nhớ yếu (như ARM), B có thể quan sát ready=1 trước mà vẫn đọc giá trị cũ của data, nên phải cưỡng chế thứ tự bằng rào chắn bộ nhớ hoặc phép toán nguyên tử mang ngữ nghĩa release/acquire. Như vậy, nếu chỉ tin vào coherence mà bỏ qua consistency thì sinh ra lỗi khó tái hiện "chỉ thỉnh thoảng mới sai", nên nhận thức rõ sự phân vai giữa hai khái niệm là điểm khởi đầu của lập trình song song.
Các chính sách đạt được những điều kiện này chia thành hai nhánh lớn. Vô hiệu hóa khi ghi (write-invalidate) vô hiệu hóa mọi bản sao khác ngay trước khi ghi để lõi ghi trở thành chủ sở hữu duy nhất; hầu như mọi bộ xử lý thương mại ngày nay đều áp dụng. Cập nhật khi ghi (write-update) phát quảng bá giá trị đã ghi tới các bản sao khác để cập nhật, nhưng khi có nhiều bên chia sẻ thì lưu lượng bus quá lớn nên hiếm khi được dùng trong hệ hiện đại. Ví dụ với mẫu nhà sản xuất-người tiêu dùng, nơi chỉ một bên ghi còn bên kia đọc ngay, update có thể có lợi, nhưng với các workload tổng quát mà lõi ghi thay đổi thường xuyên thì invalidate hiệu quả vượt trội về lưu lượng và trên thực tế đã thành chuẩn.
3. Phân loại giao thức: Snooping vs dựa trên Directory
Tùy vào "ai truyền lưu lượng nhất quán và truyền thế nào", giao thức chia thành snooping và dựa trên directory. Vì lựa chọn này thay đổi mạnh hiệu năng và khả năng mở rộng tùy theo số lõi và cấu trúc liên kết, nên phải hiểu các đánh đổi thiết kế cùng với lý do của chúng.
Phương thức snooping (snoopy) cho mọi cache thường trực giám sát bus chung (hoặc liên kết có thể quảng bá), và khi một lõi đưa lên yêu cầu đọc/ghi cho một địa chỉ nhất định, mỗi cache tự kiểm xem mình có giữ khối đó không và tự đổi trạng thái. Không có bộ quản lý trung tâm nên hiện thực đơn giản và độ trễ ngắn, nhưng vì phải quảng bá mọi yêu cầu tới toàn bộ cache nên băng thông bus bão hòa khi số lõi tăng. Do đó nó hợp với hệ quy mô nhỏ-vừa (vài đến vài chục lõi), và chip thương mại thực tế đặt snoop filter để giảm lãng phí băng thông, không gửi snoop tới các cache không thể giữ khối đó.
Phương thức dựa trên directory (Directory) đặt với mỗi khối bộ nhớ một directory ghi lại "node/cache nào đang giữ bản sao của khối đó", và lõi cần ghi truy vấn directory rồi gửi thông điệp vô hiệu hóa chọn lọc chỉ tới các bên chia sẻ. Vì không có quảng bá nên nó mở rộng được cả tới hệ NUMA/manycore quy mô lớn hàng trăm đến hàng nghìn lõi, nhưng bước gián tiếp là tra cứu directory làm tăng độ trễ và không gian lưu directory (số khối × số bit theo node) trở thành chi phí phụ. CPU máy chủ lớn ngày nay thường trộn hai phương thức, trong một socket xử lý bằng snooping/ring-mesh, giữa các socket bằng directory/snoop filter — cấu trúc lai.
| Phân loại | Snooping | Directory |
|---|---|---|
| Cách truyền | Quảng bá toàn bộ | Gửi chọn lọc chỉ tới bên chia sẻ |
| Liên kết | Bus/ring chung | Mesh/crossbar mở rộng |
| Khả năng mở rộng | Vài đến vài chục lõi | Hàng trăm đến hàng nghìn lõi |
| Độ trễ | Ngắn (giám sát trực tiếp) | Tương đối dài (qua directory) |
| Chi phí phụ | Bão hòa băng thông bus | Không gian lưu directory |
| Ví dụ áp dụng | Desktop/máy chủ nhỏ | NUMA lớn/HPC/manycore |
Ví dụ máy chủ x86 2 socket bó các lõi trong một socket bằng ring/mesh điều phối bởi snoop filter, còn giữa các socket dùng thông tin kiểu directory trên UPI/Infinity Fabric để chặn các snoop liên socket không cần thiết. Ngược lại, node HPC quy mô hàng nghìn lõi hay cấu hình đa chip của GPU khiến phương thức dựa trên directory trên thực tế là bắt buộc.
Cảm nhận bằng con số về khả năng mở rộng cũng quan trọng. Trong snooping quảng bá thuần túy, khi có N lõi thì về nguyên tắc một yêu cầu ghi phải được truyền tới cả N cache, nên lưu lượng nhất quán tăng tỷ lệ (hoặc hơn) với số lõi. Ở 8 lõi thì còn chịu được nhưng ở 64 hay 128 lõi thì liên kết bão hòa vì yêu cầu, sinh ra nghịch lý "càng thêm lõi càng chậm hơn". Phương thức directory giảm nhẹ điều này bằng cách chỉ gửi thông điệp tới tập bên chia sẻ, nhưng đổi lại phải lưu bitmap bên chia sẻ theo từng khối, làm tăng chi phí bộ nhớ. Vì vậy hệ quy mô lớn tiết kiệm không gian bằng directory thưa (sparse directory) chỉ theo dõi các khối thực sự đã nạp vào cache thay vì toàn bộ khối, hoặc bằng directory phân tầng. Đánh đổi "giảm lưu lượng thì dùng nhiều không gian lưu hơn; tiết kiệm không gian lưu thì độ chính xác (độ tinh của theo dõi) giảm" chính là căng thẳng cốt lõi của thiết kế phần cứng nhất quán.
4. Trạng thái và chuyển trạng thái của MESI
Tiêu biểu cho snooping theo hướng vô hiệu hóa là MESI, trong đó mỗi dòng cache giữ một trong bốn trạng thái. Sơ đồ chuyển trạng thái dưới đây cho thấy một dòng di chuyển ra sao theo lần đọc/ghi của lõi mình (PrRd/PrWr) và yêu cầu của lõi khác quan sát được trên bus (BusRd/BusRdX).
stateDiagram-v2
[*] --> Invalid
Invalid --> Exclusive: PrRd / khong co ban sao khac
Invalid --> Shared: PrRd / co ban sao khac
Invalid --> Modified: PrWr / BusRdX
Exclusive --> Modified: PrWr / khong can luu luong bus
Exclusive --> Shared: quan sat BusRd cua loi khac
Exclusive --> Invalid: quan sat BusRdX cua loi khac
Shared --> Modified: PrWr / vo hieu hoa ban sao khac qua BusRdX
Shared --> Invalid: quan sat BusRdX cua loi khac
Modified --> Shared: BusRd loi khac / chia se sau ghi
Modified --> Invalid: BusRdX loi khac / thuc hien write-back
Modified (M, đã sửa) là trạng thái chỉ cache này giữ bản sao duy nhất và nó mới hơn (dirty) bộ nhớ chính. Vì khối này không có ở cache khác nên có thể đọc ghi tự do, nhưng khi dòng này bị thay thế hoặc lõi khác yêu cầu thì bắt buộc phải viết lại (write-back) vào bộ nhớ hoặc chuyển dữ liệu trực tiếp cho bên yêu cầu (cache-to-cache transfer) để duy trì nhất quán.
Exclusive (E, độc quyền) là trạng thái chỉ cache này giữ bản sao nhưng giá trị bằng với bộ nhớ (clean). Giá trị của trạng thái E nằm ở tối ưu ghi. Vì đã xác định là chủ sở hữu duy nhất, khi ghi vào khối này không cần vô hiệu hóa cache khác nên có thể chuyển thẳng sang M mà không có lưu lượng bus. Nếu không có trạng thái E (giao thức MSI) thì ngay cả khi ghi vào dữ liệu đã đọc một mình cũng phải phát quảng bá vô hiệu hóa không cần thiết; trong mẫu thường gặp "đọc rồi ghi ngay", E loại bỏ sự lãng phí này và nâng hiệu năng.
Lưu ý rằng trạng thái E đặc biệt hiệu quả trong các workload ít chia sẻ dữ liệu. Trong ứng dụng điển hình nơi một luồng đơn xử lý phần lớn dữ liệu một mình, gần như mọi dòng được nạp ở E và không phát sinh lưu lượng vô hiệu hóa khi ghi, nên lượng dùng bus giảm rõ so với MSI. Ngược lại, trong workload chia sẻ thường xuyên thì lợi ích của E thu nhỏ lại, và khi đó O của MOESI hay F của MESIF bổ trợ bằng cách giảm lưu lượng trong tình huống chia sẻ. Như vậy tính hữu dụng của tập trạng thái thay đổi theo đặc tính chia sẻ của workload, nên nhà thiết kế bộ xử lý chọn giao thức dựa trên workload tiêu biểu của thị trường mục tiêu.
Shared (S, chia sẻ) là trạng thái nhiều cache cùng giữ một bản sao clean giống nhau. Đọc thì tự do, nhưng để ghi thì trước tiên phải vô hiệu hóa mọi bên chia sẻ khác bằng yêu cầu BusRdX (hoặc Upgrade) để độc chiếm quyền sở hữu rồi mới chuyển sang M. Invalid (I, vô hiệu) là trạng thái không có bản sao hoặc nó đã cũ không dùng được, nên để truy cập phải lấy lại từ bộ nhớ hoặc cache khác.
Hãy lần theo một kịch bản xem các trạng thái này thực sự di chuyển thế nào. Khởi đầu từ trạng thái không ai giữ một dòng (I), khi lõi 0 lần đầu đọc khối đó, vì không có bản sao khác nên nó được nạp ở trạng thái E. Lúc này nếu lõi 0 ghi ngay thì nó lên M mà không có lưu lượng bus. Tiếp đó khi lõi 1 đọc cùng khối, một BusRd được quan sát, và lõi 0 cung cấp dòng M của mình cho lõi 1 (cache-to-cache) trong khi cả hai cache đều hạ xuống S. Khi lõi 1 lại ghi vào khối đó, nó vô hiệu hóa (I) bản sao S của lõi 0 qua BusRdX và tự mình thành M. Rốt cuộc "quyền sở hữu" đã chuyển từ lõi 0 sang lõi 1, và nếu hai lõi ghi luân phiên lặp lại thì dòng cứ ping-pong vô tận giữa hai bên ở trạng thái M, gây độ trễ nghiêm trọng. Hình dung được dòng chảy này trong đầu mới có thể giải thích định lượng vì sao tranh chấp dữ liệu chia sẻ bào mòn hiệu năng song song.
Một điều cần bổ sung là bốn trạng thái ổn định trong sách giáo khoa (M/E/S/I) là mô hình khái niệm, còn trong hiện thực phần cứng thực tế có nhiều trạng thái quá độ (transient state) mà một dòng lưu lại trong khi yêu cầu đi qua lại trên bus. Ví dụ trạng thái trung gian chờ đáp ứng sau khi gửi yêu cầu Upgrade để lên từ S sang M, hay trạng thái trong lúc trao dòng M cho lõi khác. Các trạng thái quá độ này cần để tuần tự hóa không mâu thuẫn cuộc đua (race) khi hai lõi đồng thời nhắm quyền sở hữu cùng một dòng, và vì thế số trạng thái thực tế của giao thức nhất quán thương mại lên tới hàng chục và trở thành đối tượng của kiểm chứng hình thức (formal verification). Trong bài làm, nhắc tới cấu trúc "4 trạng thái ổn định + nhiều trạng thái quá độ" có thể cho thấy độ sâu của hiểu biết.
Các biến thể mở rộng MESI đều nhắm vào một điểm kém hiệu quả cụ thể. MOESI thêm trạng thái Owned (O), cho phép chia sẻ một khối dirty với cache khác mà không phải viết lại ngay vào bộ nhớ (chủ sở hữu chịu trách nhiệm cung cấp giá trị mới nhất). Điều này giảm lưu lượng write-back và được dòng AMD áp dụng. MESIF thêm trạng thái Forward (F), chỉ định rằng chỉ "một" trong nhiều bên chia sẻ đáp ứng yêu cầu (cung cấp dữ liệu), qua đó loại bỏ va chạm đa đáp ứng (dòng Intel). Như vậy các trạng thái bổ sung đều xuất phát từ cùng động cơ là giảm một loại lưu lượng cụ thể — hoặc "viết lại bộ nhớ không cần thiết" hoặc "đáp ứng trùng lặp" — và khác biệt là lựa chọn "giảm chi phí nào trước".
| Trạng thái | Hợp lệ | Khớp bộ nhớ | Chia sẻ được với cache khác | Hành vi khi ghi |
|---|---|---|---|---|
| Modified | O | Không khớp(dirty) | X(độc nhất) | Có thể ngay |
| Exclusive | O | Khớp(clean) | X(độc nhất) | Chuyển M không lưu lượng |
| Shared | O | Khớp(clean) | O | Chuyển M sau khi vô hiệu hóa |
| Invalid | X | - | - | Cần nạp lại |
5. Chuyên sâu: Chia sẻ giả (False Sharing) và xu hướng mới nhất
Lý do thực chiến nhất để phải hiểu giao thức nhất quán là cái bẫy hiệu năng mang tên chia sẻ giả (false sharing). Nhất quán cache hoạt động không theo đơn vị byte mà theo đơn vị dòng cache (thường 64 byte). Do đó ngay cả khi hai luồng khác nhau xử lý những biến hoàn toàn khác nhau về mặt logic, nếu các biến đó tình cờ được đặt trong cùng một dòng 64 byte, lần ghi của một luồng sẽ vô hiệu hóa dòng của luồng kia và ping-pong xảy ra. Ví dụ dù đặt mảng bộ đếm theo lõi long cnt[N] liền kề và mỗi lõi chỉ tăng chỉ số của mình, nhiều bộ đếm bị bó vào một dòng nên dù thực sự không có chia sẻ, lưu lượng nhất quán cực lớn vẫn phát sinh. Trong đo đạc thực tế, loại mã này thường nhanh hơn vài đến vài chục lần khi đệm (padding) mỗi bộ đếm tới kích thước dòng cache (alignas(64)), và đây là điểm tinh chỉnh cốt lõi của mã song song hiệu năng cao.
Ước lượng chi phí chia sẻ giả theo đơn vị chu kỳ làm rõ vì sao nó chí mạng. Một cache hit L1 thường kết thúc trong vài chu kỳ, nhưng lấy về một dòng mà lõi khác đang giữ ở trạng thái M — một remote HITM (Hit-Modified) — tốn hàng chục đến hàng trăm chu kỳ. Nếu socket khác nhau thì còn đi qua liên kết liên socket nên chi phí càng lớn. Với mã nơi bốn luồng mỗi luồng tăng, hàng trăm triệu lần mỗi giây, các bộ đếm khác nhau bị bó vào một dòng, thì mỗi lần tăng kích hoạt một lần chuyển quyền sở hữu dòng và trên thực tế bị tuần tự hóa, chịu độ trễ gấp hàng chục lần so với cache hit. Ngược lại, tách các bộ đếm sang các dòng khác nhau cho phép mỗi lõi độc chiếm dòng của mình ở trạng thái M và cập nhật mà không có lưu lượng bus, nên nó mở rộng gần như tuyến tính. Hiện tượng cùng một thuật toán khác nhau hàng chục lần chỉ vì một cách bố trí dữ liệu này cho thấy rõ nhất mệnh đề rằng, trong thời đại đa lõi, "phần cứng quyết định tính đúng đắn, bố trí dữ liệu quyết định hiệu năng".
Nguyên lý này được phản ánh khắp các codebase công nghiệp thực tế. Nhân Linux dùng rộng rãi biến per-CPU giữ một bản sao độc lập theo từng lõi để tránh triệt để việc ghi chia sẻ và lưu lượng nhất quán kéo theo, gộp thống kê và bộ đếm theo từng lõi rồi chỉ cộng lại khi đọc. Trong Java, chú giải @Contended (JEP 142) đệm các trường nóng theo ranh giới dòng cache để ngăn chia sẻ giả, và vòng đệm (ring buffer) của thư viện nhắn tin hiệu năng cao LMAX Disruptor cũng nổi tiếng vì đệm các bộ đếm thứ tự để loại bỏ chia sẻ giả giữa nhà sản xuất và người tiêu dùng. Như vậy nguyên tắc "giảm chia sẻ, và với chia sẻ không tránh được thì tách dòng" là bài bản thiết kế hiệu năng lặp lại ở mọi tầng — OS, runtime, thư viện.
Ngoài ra chi phí của khóa (lock) và phép toán nguyên tử (atomic) cũng được giải thích bằng giao thức nhất quán. Việc giành được compare-and-swap hay một spinlock đòi hỏi độc chiếm dòng chứa biến khóa ở trạng thái ghi được (M), nên khi nhiều lõi tranh một khóa thì dòng đó di chuyển như một cuộc chạy loạn giữa các lõi. Đây là lý do spinlock truyền thống chậm đi gấp bội theo số lõi, và cũng là bối cảnh ra đời của các khóa dựa trên hàng đợi như MCS lock và ticket lock, được thiết kế để spin trên biến cục bộ theo từng lõi. Tức là khi hiểu rằng dưới đáy hiện tượng "khóa chậm" chính là lưu lượng nhất quán cache, ta có thể chọn các toa thuốc giảm tranh chấp (sharding, backoff, gộp cục bộ) dựa trên nguyên lý.
Về xu hướng mới nhất, thứ nhất, việc mở rộng/chia sẻ bộ nhớ dựa trên CXL (Compute Express Link) được chú ý. Các giao thức CXL.cache/CXL.mem mở rộng nhất quán phần cứng giữa CPU và bộ tăng tốc/bể bộ nhớ ngoài, hướng tới cấu trúc nơi thiết bị cache bộ nhớ host một cách nhất quán hoặc nơi một bể bộ nhớ được nhiều host truy cập nhất quán. Đây là nỗ lực mở rộng miền nhất quán, vốn truyền thống bị giam trong một socket, lên mức package và rack, và nó ăn khớp với nhu cầu trung tâm dữ liệu muốn tái phân bổ linh hoạt bộ nhớ giữa các máy chủ thông qua tách rời bộ nhớ (memory disaggregation) và gom bể (pooling). Tuy nhiên phạm vi nhất quán càng rộng thì độ trễ truy cập từ xa và độ khó quản lý lưu lượng nhất quán càng tăng theo, nên đòi hỏi thiết kế phân tầng thận trọng về việc đặt dữ liệu nào vào diện chia sẻ nhất quán. Thứ hai, với xu hướng chiplet và manycore, số node trong một package đơn tăng vọt, snooping quảng bá thuần túy chạm giới hạn và directory/snoop filter cùng liên kết mesh đang thành chuẩn. Thứ ba, trong GPU và tính toán dị thể, một điểm tranh luận thiết kế là phần cứng bảo đảm phạm vi nhất quán của bộ nhớ hợp nhất CPU-GPU (ví dụ không gian địa chỉ ảo hợp nhất) đến đâu và từ đâu giao cho đồng bộ hóa phần mềm. Thứ tư, khi lưu lượng nhất quán chiếm phần đáng kể của hiệu năng và điện năng, nghiên cứu về directory có khả năng mở rộng (sparse/hierarchical directory) và phân chia miền nhất quán vẫn tiếp tục. Thứ năm, bộ nhớ giao dịch phần cứng (HTM) tận dụng khả năng theo dõi của giao thức nhất quán, để phần cứng phát hiện va chạm giữa các dòng cache được đọc và ghi trong vùng giao dịch và rollback khi va chạm, hỗ trợ đồng bộ hóa không khóa. Điều này cho thấy xu hướng cơ chế nhất quán mở rộng vượt khỏi việc chỉ bảo đảm tính đúng đắn, thành hạ tầng nền cho kiểm soát đồng thời.
Điểm chung của các xu hướng này là "trong thời đại lõi và bộ nhớ tăng bùng nổ, thay vì cứ áp đặt nhất quán phần cứng toàn diện như cũ, phạm vi được điều tiết một cách thông minh". Trọng tâm đang dịch từ nhất quán dày đặc của máy chủ đơn truyền thống sang "nhất quán chọn lọc" phân tầng phạm vi theo socket, package, rack và giao một phần cho phần mềm, và nhà thiết kế phải phân tích mẫu chia sẻ của workload để phán đoán đặt nhất quán phần cứng tới tầng nào.
6. Cân nhắc và hàm ý
Tổng hợp lại, giao thức nhất quán cache là "một khế ước vô hình chống đỡ nền móng của hiệu năng đa lõi". Các hàm ý rút ra từ góc nhìn của Kỹ sư chuyên nghiệp như sau.
Thứ nhất, phải tư duy bằng cách tách biệt bảo đảm tính đúng đắn của phần cứng khỏi trách nhiệm hiệu năng của phần mềm. MESI bảo đảm coherence (khả năng thấy giá trị mới nhất của một địa chỉ đơn), nhưng thứ tự giữa các biến khác nhau thuộc lĩnh vực của mô hình consistency bộ nhớ và các rào chắn. Trong đồng bộ hóa tinh tế như mã lock-free hay double-checked locking, ngộ nhận rằng "vì nhất quán đã được bảo đảm nên không cần rào chắn" dẫn tới lỗi chí mạng. Từ góc nhìn Kỹ sư chuyên nghiệp, năng lực thiết kế và rà soát bằng cách phân biệt hai tầng này là điều bắt buộc.
Thứ hai, phải lấy việc bố trí dữ liệu là biến quyết định của hiệu năng làm nguyên tắc thiết kế. Tránh chia sẻ giả (tách/đệm biến nóng), khớp tính cục bộ của luồng và dữ liệu (bố trí NUMA-aware), và tối thiểu hóa ghi chia sẻ (gộp cục bộ theo từng lõi rồi hợp nhất) chi phối khả năng mở rộng song song. Như một đánh đổi, đệm làm tăng lượng bộ nhớ dùng, nên cần phán đoán áp dụng nó một cách chọn lọc cho dữ liệu nóng nơi tranh chấp thực sự gay gắt.
Thứ ba, phải tinh chỉnh khác nhau theo đặc tính của từng kiến trúc (MESI/MESIF/MOESI, snooping đối với directory). Cùng một mã nhưng với Intel (MESIF) và AMD (MOESI), số socket, cài đặt SNC, dáng dấp lưu lượng nhất quán thay đổi. Khi lập hồ sơ hiệu năng, phải quan sát không chỉ cache miss mà cả các bộ đếm liên quan nhất quán (ví dụ remote HITM, lưu lượng cross-socket) để truy nguyên nguyên nhân nghẽn cổ chai ở lưu lượng chéo/chia sẻ.
Thứ tư, từ góc độ khả năng mở rộng, phải điều tiết phạm vi nhất quán (coherence domain) ở mức thiết kế. Lõi, socket, bộ tăng tốc càng nhiều thì nhất quán phần cứng toàn diện càng tăng vọt chi phí, nên việc cố ý thu hẹp phạm vi nhất quán hoặc thay thế bằng phần mềm — như CXL, bộ nhớ chia sẻ phân tán, truyền thông điệp (MPI) — là hữu hiệu. Trong môi trường bộ nhớ dị thể và gom bể tương lai, "bó bằng nhất quán phần cứng tới đâu và tách bằng đồng bộ hóa tường minh từ đâu" dự kiến trở thành quyết định cốt lõi của kiến trúc hệ thống, và như các công nghệ liên đới phải cân nhắc cùng nhau tối ưu tính cục bộ NUMA, rào chắn bộ nhớ/phép toán nguyên tử, và bộ nhớ giao dịch.
Thứ năm, phải nhận thức cả tác dụng phụ của cơ chế nhất quán ở mặt an ninh và độ tin cậy. Chuyển trạng thái cache và chênh lệch độ trễ truy cập do nó gây ra trở thành tín hiệu đo được, và có thể thành nền tảng cho các tấn công kênh phụ cache (ví dụ Flush+Reload, đo dựa trên lưu lượng nhất quán) suy luận mẫu truy cập bộ nhớ của lõi khác. Ngoài ra giao thức nhất quán làm việc gỡ lỗi đồng thời đa lõi trở nên khó, vì các điều kiện đua (race) khó tái hiện chỉ lộ ra ở những bố trí lõi và thời điểm nhất định. Từ góc nhìn Kỹ sư chuyên nghiệp, cân nhắc sự cân bằng giữa tối ưu hiệu năng và khả năng an ninh/kiểm chứng, phải đưa hiện thực thời gian hằng cho các phép toán nhạy cảm hoặc kiểm thử đồng thời có hệ thống (kiểm tra mô hình, kiểm thử áp lực) vào thiết kế.
Tài liệu tham khảo
- Hennessy & Patterson, "Computer Architecture: A Quantitative Approach" (Chapter on Multiprocessors and Cache Coherence)
- Sorin, Hill, Wood, "A Primer on Memory Consistency and Cache Coherence", Morgan & Claypool — https://doi.org/10.2200/S00346ED1V01Y201104CAC016
- Intel, "Intel 64 and IA-32 Architectures Software Developer's Manual, Vol. 3A" — https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- CXL Consortium, "Compute Express Link Specification" — https://computeexpresslink.org/
Tóm tắt một câu: Giao thức nhất quán cache (MESI) quản lý các bản sao của cùng một khối rải rác khắp các cache bằng máy trạng thái Modified·Exclusive·Shared·Invalid để bảo đảm bằng phần cứng khả năng thấy "một giá trị mới nhất duy nhất", và lựa chọn giữa snooping với directory cùng việc tránh chia sẻ giả chi phối hiệu năng và khả năng mở rộng đa lõi.