Rào Cản Bộ Nhớ Trong Môi Trường Đa Nhân - nói dối e blog

Rào Cản Bộ Nhớ Trong Môi Trường Đa Nhân

Ban đầu tôi không định viết ngay về Hội nghị Phát triển Phần mềm vừa rồi. Có quá nhiều điều để viết nên反倒 bối rối không biết bắt đầu từ đâu. Hôm nay mới có dịp lướt web, ghé qua blog của thầy Châu Vĩ Dân, thấy bài viết này. Vì trong bài có đề cập đến tôi nên muốn để lại vài dòng phản hồi. Tiếc rằng hệ thống blog của CSDN quá tệ (chủ đề này chúng tôi đã cùng nhau phê phán trong buổi thảo luận vào thứ Bảy, tạm thời không bàn đến), cuối cùng vẫn không gửi được bình luận. Đành phải viết riêng ở đây vậy.

Buổi trình bày của thầy Châu vừa vặn diễn ra ngay trước phiên của tôi. Cùng một phòng họp, nội dung cũng rất hấp dẫn nên tôi đã tham dự đầy đủ. Thực ra bài giảng khá chất lượng, chỉ có điều hơi mang phong cách giảng đường đại học, thiếu tương tác hơn một chút. Không khí hội trường không sôi nổi bằng phần trình bày sau đó của Andrei về cấu trúc dữ liệu không khóa (Lock-Free Data Structures).

Vấn đề thầy Châu trình bày lại chính là chủ đề tôi từng gặp khi xây dựng bộ phân bổ bộ nhớ an toàn đa luồng vài năm trước, có nghiên cứu kỹ. Kết hợp với việc quan tâm đến các công nghệ của Intel trong thời gian gần đây, tôi liền có ý định phát biểu. Hoàn cảnh lúc đó rất phù hợp khi có một bạn khán giả đặt câu hỏi rằng: Thực tế thì lời gọi InterlockedIncrement là thừa thãi. (Sau đó trao đổi danh thiếp mới biết anh bạn này là kỹ sư của Google).

Nếu là vài năm trước, tôi chắc chắn sẽ đồng tình với ý kiến này. Nhớ lại khoảng năm 2004, trong diễn đàn nội bộ công ty cũng từng có tranh luận tương tự. Cụ thể là trên hệ thống 32-bit, việc ghi một giá trị dword là nguyên tử (atomic). Nếu CPU đảm bảo thứ tự thực thi logic của chương trình (program ordering), thì có thể dùng thao tác ghi đơn thuần thay cho khóa. Sau khi xử lý xong một khối dữ liệu lớn, chỉ cần cập nhật đánh dấu cuối cùng là đủ đảm bảo an toàn mà không cần khóa. (Điều kiện ẩn là dữ liệu phải được căn chỉnh 32-bit).

Nhắc lại, trong buổi trình bày của Andrei về cấu trúc dữ liệu không khóa, ông ấy cũng đặt câu hỏi tương tự: Tại sao hazard list phải dùng danh sách liên kết đơn? Chính là vì con trỏ danh sách có thể được cập nhật nguyên tử mà không cần khóa.

Điều này đúng trong thời đại đơn nhân vì CPU đơn nhân yêu cầu tính nhất quán (self-consistent) cho các thao tác đọc/ghi. Giải thích thêm, các CPU hiện đại có thể thực thi lệnh không theo thứ tự lập trình (program ordering) - công nghệ thực thi không tuần tự (out-of-order execution) giúp tối ưu hiệu suất pipeline. CPU đơn nhân đảm bảo tính nhất quán bằng cách đợi đến khi dữ liệu thực sự được đọc thì mới đảm bảo đúng thứ tự logic.

Vấn đề nằm ở đây: Khi chuyển sang đa nhân để tối ưu pipeline trên từng nhân, tính nhất quán toàn hệ thống không còn được đảm bảo. Mỗi nhân có thể thực thi lệnh không theo thứ tự, dẫn đến việc dữ liệu được ghi logic sau lại có thể thực sự ghi vào bộ nhớ trước. Khi nhìn toàn cảnh hệ thống đa nhân, tính nhất quán không còn tồn tại.

Nói cách khác, nếu không có biện pháp bảo vệ, khi một nhân ghi xong dữ liệu rồi đánh dấu hoàn thành, nhân khác kiểm tra dấu hiệu này để xác nhận dữ liệu đã sẵn sàng - chiến lược này không đáng tin. Dấu hiệu có thể được ghi trước khi dữ liệu thực sự cập nhật vào bộ nhớ.

Giải pháp là buộc CPU tuần tự hóa bằng lệnh rào cản trước khi ghi dấu hiệu. InterlockedIncrement và các hàm tương tự cung cấp tính năng này. Dịch sang lệnh Intel, ta thấy chúng dùng tiền tố lock trong mã hợp ngữ. Khi thực thi các lệnh truy cập bộ nhớ có lock, CPU phát tín hiệu lock# trên bus, chặn các yêu cầu truy cập bộ nhớ khác.

Tuy nhiên hiệu suất giải pháp này thấp. Lệnh lock ảnh hưởng đến bus sẽ ngày càng kém hiệu quả khi số nhân tăng lên. Có thể hình dung mỗi khi một nhân phát lock#, gần như toàn bộ nhân khác phải tạm dừng (trừ khi không truy cập bộ nhớ). Với 2-4 nhân hiện nay, ảnh hưởng gần như không đáng kể, nhưng với 32-64 nhân thì hậu quả sẽ nghiêm trọng.

Chú ý: Cách giải thích trên chưa hoàn toàn chính xác. Vì lệnh khóa bộ nhớ được dùng rộng rãi trong lập trình đa luồng, các nhà thiết kế chip đã tối ưu. Từ Pentium Pro trở đi, nếu vùng nhớ được cache, lock# sẽ không phát trên bus mà chỉ khóa cache. Chi phí giảm đáng kể, nhưng vẫn còn cao khi nhiều nhân cùng cache một vùng nhớ.

Giải pháp nhẹ hơn là dùng lệnh CPUID để tuần tự hóa. Đến Pentium III, Intel bổ sung lệnh SFENCE trong tập lệnh IA32, cho phép kiểm soát tinh tế hơn với chi phí thấp hơn. Chèn SFENCE vào chuỗi lệnh sẽ đảm bảo mọi thao tác ghi trước đó hoàn tất (các lệnh không ghi vẫn có thể thực thi không tuần tự). Khi nhân khác đọc cùng vùng nhớ, gần như không xảy ra lỗi.

Từ Pentium 4 trở đi, nghiêm ngặt hơn, cần kết hợp LFENCE (không có trên Pentium III) để đảm bảo các thao tác đọc trước đó theo thứ tự logic đã hoàn tất. Ngoài ra còn có lệnh MFENCE mạnh hơn, đảm bảo cả đọc và ghi đều hoàn tất.

Ban đầu tôi định viết dựa trên trí nhớ, nhưng sau khi kiểm tra lại Chương 7: Quản lý đa xử lý trong tài liệu “IA-32 Intel Architecture Software Developer’s Manual Volume 3” năm 2005, tôi tin rằng nội dung đã chính xác. Nếu còn sai sót nhỏ, mong các chuyên gia lượng thứ. Bạn đọc thực sự quan tâm nên tham khảo tài liệu gốc của Intel để có thông tin chính xác và đầy đủ. Xin lỗi vì không thể chỉ dẫn cụ thể cách tìm tài liệu, hãy hỏi Google nhé!

0%