Vấn Đề Căn Chỉnh Bộ Nhớ Và Tối Ưu Trình Biên Dịch - nói dối e blog

Vấn Đề Căn Chỉnh Bộ Nhớ Và Tối Ưu Trình Biên Dịch

Vấn đề căn chỉnh bộ nhớ và tối ưu hóa trình biên dịch
Hôm qua, trên nhóm nội bộ công ty mang tên “Tự rước họa vào thân” (dành cho các lập trình viên), có thành viên đã chia sẻ một bài viết trên Zhihu. Điều khiến anh ấy ngạc nhiên là vấn đề hóa ra lại được giải quyết bằng cách tắt chức năng builtin-memset của GCC, một cách vô cùng huyền bí.

Tôi nói rằng cảm giác đó mới chính là bình thường. Về phần trong bài viết có đoạn: “Khi thêm tùy chọn biên dịch -no-builtin-memset, mọi thứ trở nên ổn định. Mọi người vừa vui mừng vừa học được thêm kiến thức mới.”, tôi cho rằng cảm giác “vui mừng như trút được gánh nặng” này đối với lập trình viên mà nói mới thật sự là bất thường. Phản ứng đúng lẽ ra phải là “càng thêm nghi hoặc” mới phải lý!

Cơ sự ra sao? Vì bài viết không cung cấp đủ thông tin nên khó lòng phán đoán chính xác. Tuy nhiên theo trực giác, tôi cảm thấy vấn đề này rất giống với một chủ đề khác chúng tôi từng tranh luận gay gắt trên nhóm “Tự rước họa vào thân” vài tháng trước.

Tôi nghi ngờ đây là lỗi do bộ nhớ không được căn chỉnh đúng cách. Trước đây, có đồng nghiệp phát hiện thấy thư viện tuần tự hóa (serialization) tôi viết cho dự án Skynet có thể gây crash trên nền tảng ARM 32-bit. Vì từ đầu Skynet đã được ứng dụng rộng rãi trên các nền tảng nhúng, nên khi thiết kế thư viện tôi đặc biệt lưu ý đến tính tương thích đa nền tảng. Tôi từng nghĩ chỉ cần sử dụng memcpy thay vì truy cập trực tiếp địa chỉ thông qua con trỏ thì có thể tránh được các vấn đề liên quan đến căn chỉnh bộ nhớ.

Tuy nhiên sau sự cố đó, tôi đã dành thời gian đọc kỹ chuẩn C, tra cứu nhiều tài liệu để hiểu sâu hơn về cơ chế căn chỉnh bộ nhớ trong ngôn ngữ C. Đặc biệt, chuẩn C1x còn bổ sung thêm thư viện stdalign.h với từ khóa alignas giúp người lập trình kiểm soát chi tiết việc căn chỉnh.

Trình biên dịch C hiện đại có khả năng tối ưu hóa cực kỳ sâu, dựa trên các quy tắc cho phép trong tiêu chuẩn ngôn ngữ. Rất nhiều đoạn mã nhìn qua tưởng như gọi hàm nhưng thực chất đã được tối ưu thành mã máy đơn giản hơn nhiều. Điển hình nhất là các hàm tối ưu hóa memset, memcpy, memmove.

Chúng ta đều biết rằng ngay cả khi phần cứng không báo lỗi, việc truy cập bộ nhớ không căn chỉnh cũng gây tổn thất hiệu năng nghiêm trọng. Mà nếu đã gây lỗi thì cực kỳ khó debug. Vì vậy chúng ta phải hết sức tránh việc truy cập vào vùng nhớ không căn chỉnh.

Theo tài liệu từ kernel.org:

Một số kiến trúc phần cứng có thể xử lý truy cập bộ nhớ không căn chỉnh một cách trong suốt, nhưng thường đi kèm chi phí hiệu năng rất lớn.
Một số kiến trúc khác sẽ phát sinh lỗi phần cứng khi gặp truy cập không căn chỉnh. Hệ thống xử lý lỗi có thể khắc phục, nhưng cũng làm giảm đáng kể hiệu suất.
Có những kiến trúc không hỗ trợ truy cập bộ nhớ không căn chỉnh, nhưng lại thực hiện một truy cập khác không mong muốn mà người dùng không hề hay biết, dẫn đến lỗi ẩn sâu khó phát hiện!

Các hàm chuẩn như memcpymemset nếu được cung cấp dưới dạng hàm thư viện thì bắt buộc phải kiểm tra việc căn chỉnh địa chỉ tại runtime, từ đó lựa chọn phiên bản xử lý phù hợp. Nhưng khi trình biên dịch can thiệp tối ưu hóa, nó có thể suy diễn thêm nhiều thông tin dựa trên ngữ cảnh: địa chỉ có được căn chỉnh không? Độ dài thao tác có phải hằng số không? Dựa vào dữ liệu này, trình biên dịch có thể loại bỏ nhiều rẽ nhánh kiểm tra không cần thiết tại runtime.

Chính các quy tắc ngôn ngữ lại hỗ trợ trình biên dịch trong việc suy diễn này. Ví dụ, dù memcpy nhận tham số kiểu void*, nhưng nếu trong ngữ cảnh, con trỏ này được chuyển đổi từ kiểu int64* thì trình biên dịch sẽ suy luận địa chỉ này chắc chắn được căn chỉnh theo chuẩn 64-bit. Đây là hành vi hợp lệ theo tiêu chuẩn ngôn ngữ. Vì vậy, bạn không nên tùy tiện ép kiểu giữa các con trỏ có độ căn chỉnh khác nhau.

Nói quay lại, nếu bạn thực sự gặp lỗi do trình biên dịch tối ưu hóa, bạn nên làm gì? Theo tôi, tư duy đúng là phải truy tìm tận cùng nguyên nhân. Ít nhất, hãy yêu cầu trình biên dịch xuất ra mã hợp ngữ để đối chiếu. Nếu thấy mã máy không như kỳ vọng, trước tiên hãy nghi ngờ xem mã nguồn của bạn có vi phạm tiêu chuẩn nào không, khiến trình biên dịch hiểu sai ý định của bạn. Nếu quả thật là lỗi của trình biên dịch, hãy gửi báo cáo lỗi đến cộng đồng phát triển của trình biên dịch đó, hỗ trợ họ khắc phục. Việc tìm cách workaround chỉ nên là giải pháp tạm thời, không nên ảnh hưởng đến tiến độ phát hành phần mềm của bạn.

Nhân tiện, cách đây khoảng 1 năm, trong quá trình hoàn thiện Lua 5.4, cộng đồng Lua đã phát hiện một lỗi nghiêm trọng do GCC tối ưu hóa sai. Ngay lập tức họ đã gửi thông báo đến cộng đồng GCC, và vấn đề đã được xử lý nhanh chóng.

Trong phát triển phần mềm hiện đại, các mắt xích từ trên xuống dưới đều gắn kết mật thiết với nhau. Chỉ khi tất cả các bên cùng nhau cảnh giác với từng lỗi tiềm ẩn, toàn bộ hệ thống mới có thể ngày càng hoàn thiện hơn.

0%