RAII, ảo tưởng của Rust/Linux
(kristoff.it)Tóm tắt
Đây là bài viết được viết sau khi theo dõi cuộc tranh cãi giữa các nhà phát triển Rust và các nhà phát triển Linux hiện có. Nhiều nhà phát triển có thể có phong cách lập trình khác nhau, nhưng dự án Linux trước đây đã từng loại trừ C++ để tránh phong cách và cấu trúc mã của nó (RAII).
Cách vận hành của đoạn mã mà Asahi Lina nhắc đến quá chậm khi kết thúc chương trình đó, và đi ngược lại với xử lý theo lô, phương pháp nền tảng nhất để xây dựng phần mềm định hướng hiệu năng. Ví dụ, dùng vùng nhớ để xử lý theo lô có thể điều phối nhiều vòng đời thành một, nên không cần RAII.
Dưới đây là các tài liệu tôi đưa ra để củng cố lập luận của mình. Tất cả chúng đều nói về lý do vì sao xử lý theo lô là tốt:
- Casey Muratori | Smart-Pointers, RAII, ZII? Becoming an N+2 programmer
- CppCon 2014: Mike Acton "Data-Oriented Design and C++"
- Modern Systems Programming: Rust and Zig - Aleksey Kladov
Vì vậy, tôi cho rằng Linux không bao giờ nên chấp nhận RAII.
Lý do tôi mang bài này đến đây là vì tôi đã nhiều lần thấy các nhà phát triển Rust ở Hàn Quốc rất tức giận khi đọc bài đó, nên tôi tò mò không biết mọi người ở đây nghĩ gì. Các bạn nghĩ sao?
11 bình luận
Theo quan điểm của tôi, nhưng tôi phần nào cũng hiểu được chủ nghĩa tinh hoa của một số lập trình viên nhất định. Từ góc độ "kỹ nghệ" phần mềm, đặc biệt là với Linux — một "phần mềm" mà ngày nay trong phe nguồn mở rất khó tìm được ví dụ nào khác vừa bắt tay rộng rãi với cả phe đóng, vừa góp phần vào sự tiến bộ của triết lý nguồn mở — có lẽ họ duy trì một thái độ bảo thủ, thậm chí trông có phần bài ngoại và giống Luddite hơn, vì lo rằng các lập trình viên chưa được kiểm chứng sẽ ồ ạt tràn vào dưới ngọn cờ Rust, chắp vá thêm những đoạn mã nằm ngoài sự kiểm soát của lực lượng nòng cốt đang bảo trì dự án hiện tại, làm tăng mạnh nợ kỹ thuật và rút ngắn vòng đời của Linux?
Thật thú vị khi để nguồn mở có thể mãi là nguồn mở, người ta lại chọn một thái độ không "mở" cho lắm.
Tôi cũng thường dùng và khuyến khích RAII hoặc các kiểu quản lý tài nguyên tương tự. Vì ngay cả khi không biết RAII là gì mà cứ dùng theo quán tính, thì ít nhất vẫn tạo ra được mã “an toàn trước đã”.
Tuy nhiên, nếu không hiểu đúng mà dùng, rất dễ sinh ra hàng loạt đoạn mã kém hiệu quả, kiểu như một file lẽ ra chỉ cần mở một lần thì lại bị mở rồi đóng đến hàng chục lần. Nếu lập trình viên luôn duy trì sự quan tâm đến hiệu năng và văn hóa đó được xem là tiền đề trong đội phát triển, thì tôi nghĩ ngay cả với RAII vẫn có thể đạt được mức hiệu năng đủ tốt.
freemỗi khi từng đối tượng bị hủyfreelại rồi thực thi theo lô?Trong Linux có chức năng nào khiến cách 2 chạy nhanh hơn cách 1 không? Kiểu như API ấy?
Tôi vốn dĩ đương nhiên vẫn sống với cách 1, nên chưa hiểu lắm.
Tôi không muốn quay lại trải nghiệm phát triển là sau khi code xong rồi mới dùng valgrind để tìm rò rỉ bộ nhớ.
Tôi không chắc lắm, nhưng việc nói sẽ không dùng RAII có vẻ như là định tận dụng rò rỉ bộ nhớ có chủ đích để kéo hiệu năng (đóng) lên; không biết đây có đúng là hướng đi hay không.
Dù sao thì nếu là lập trình viên giỏi quản lý bộ nhớ thủ công, họ cũng sẽ dùng RAII tốt; còn nếu là người không thể phát triển mà không có RAII, thì cũng sẽ không thể quản lý bộ nhớ thủ công, nên có vẻ chẳng có lý do gì để không dùng RAII.
Tò mò
freetốn bao nhiêu thời gian, nên dù khá khác với workload thực tế, tôi đã viết một đoạn mã đơn giản để thử. (Dùng bản build release của Rust, vớistd::alloc::allocvàstd::fs::File.)Tôi cấp phát 10.000.000 khối nhớ với kích thước khác nhau, tổng cộng khoảng 2,5GB, rồi chỉ đo riêng thời gian giải phóng thì mất 1,87 giây. Tính ra là 187ns cho mỗi khối.
Ngược lại với file, tôi chỉ mở khoảng 10.000 handle rồi đo riêng thời gian đóng, thì mất khoảng 9 giây. Tức là 900us cho mỗi file.
(Máy Windows này có lẽ do phần mềm diệt virus nên thao tác file chậm bất thường. Trên một laptop Windows khác thì lần lượt là 400ns/200us, còn trên một PC Linux khác là 50ns/600ns.)
Người ta thường nhắc đến xử lý hàng loạt hoặc tin vào OS khi kết thúc để cố ý làm rò rỉ resource như một phương án thay thế cho RAII. Với bộ nhớ thì điều đó có vẻ khá dễ làm.
Nhưng với các resource như file hay socket, tôi chưa từng thấy API thu hồi hàng loạt; và nếu để rò rỉ resource thì thời gian trong user code có thể giảm, nhưng đúng bằng phần giảm đó, thời gian kernel cần để kết thúc process lại tăng lên, nên lợi ích về hiệu năng cũng không đáng kể.
RAII cho bộ nhớ cũng không phải là chậm đến mức như vậy, cũng không phải kỹ thuật khiến không thể dùng arena, và cũng không phải thứ ngăn cản việc cố ý làm rò rỉ khi cần, nên có vẻ khó xem đó là lý do để né tránh RAII.
Còn với RAII cho file vốn chậm hơn, trong tình huống không có cách xử lý hàng loạt và cũng không có cách né tránh chi phí, tôi khá tò mò liệu phương án thay thế cho RAII thực sự tốt hơn được bao nhiêu.
Hơi ngoài lề một chút, nhưng tôi có cảm giác các phản biện về RAII và lifetime thường chỉ được bàn trong phạm vi resource bộ nhớ, tiêu biểu là
malloc/free.RAII và lifetime không chỉ hữu ích cho cấp phát bộ nhớ, mà còn rất hữu dụng để mô hình hóa hầu hết các loại resource cần có bước thu nhận và hoàn trả, đồng thời cần kiểm soát truy cập độc quyền trong thời gian nắm giữ, như các resource của OS gồm file, socket, lock, cũng như object pool, connection pool, v.v.
Những resource này cũng chia sẻ cùng một cấu trúc với
malloc/free, nên cũng gặp các vấn đề cùng kiểu như leak, use after free, double free;và chính vì chia sẻ cùng cấu trúc đó mà RAII và lifetime có thể giải quyết đồng thời không chỉ vấn đề của bộ nhớ mà cả vấn đề của những resource này nữa. Tôi nghĩ điểm này cần được nhấn mạnh hơn.
Ví dụ, trong Rust, ngay cả với file handle cũng có thể ngăn use after close và double close ngay tại thời điểm biên dịch:
https://play.rust-lang.org/?version=stable&mode=debug&edition=…
Các ngôn ngữ GC chủ đạo quản lý bộ nhớ bằng GC, nhưng rốt cuộc để xử lý những resource cần tính quyết định trong quản lý như file handle hay socket,
chúng vẫn phải bổ sung thêm các cấu trúc kiểu RAII (như
try-with-resourcescủa Java,usingcủa C#,withcủa Python) hoặc các cấu trúc tương tự (nhưdefercủa Go),và kết quả là một ngôn ngữ lại có nhiều chế độ quản lý resource khác nhau. So với vậy, tôi nghĩ cách này có lẽ vẫn tốt hơn.
Nếu ý bạn là arena, thì tất nhiên Rust cũng có arena, và bằng cách gắn arena với lifetime, cũng có thể cấm việc truy cập vào các phần tử của arena sau khi đã "giải phóng hàng loạt" bằng cách loại bỏ arena. Hãy tham khảo https://crates.io/keywords/arena .
Tôi hy vọng sẽ có thêm nhiều ngôn ngữ ra đời sau zig hay rust. Nhưng cho đến giờ, tôi vẫn chưa thấy ngôn ngữ nào phù hợp bằng rust. Thậm chí, tôi còn nghĩ rằng kiến thức giữa các lập trình viên nảy sinh từ những cuộc thảo luận giữa các ngôn ngữ như thế này mới thực sự hữu ích. Haha..
Tôi cũng là một lập trình viên khá thường xuyên dùng Rust làm ngôn ngữ chủ lực, nên không đến mức tức giận, nhưng quả thật tôi có cảm giác tác giả đang đưa ra một ví dụ hơi cực đoan (kiểu "khi thoát chương trình đó thì quá chậm", và ngay cả trong video được dẫn link cũng nêu một trường hợp không liên quan trực tiếp đến dự án Rust, mà là khi thoát Visual Studio thì destructor của từng thành phần riêng lẻ được gọi khiến mất quá nhiều thời gian).
Trong những trường hợp về hiệu năng mà cần xử lý việc dọn dẹp của nhiều thành phần cùng một lúc, thay vì triển khai
Dropcho từng thành phần riêng lẻ, có lẽ có thể chọn cách triển khaiDropở một kiểu giữ vòng đời của các thành phần đó để thực hiện việc dọn dẹp đồng loạt. Nếu còn đặt thêm cơ chế an toàn để các thành phần đó chỉ có thể được tạo ra thông qua API của kiểu đó thì sẽ càng tốt hơn.Tất nhiên, điều mà tác giả bài viết trên có lẽ lo ngại là nếu thông lệ sử dụng RAII đi vào codebase Linux, thì trong sự phức tạp của một codebase khổng lồ, các đoạn mã có nguy cơ gây lo ngại hiệu năng một cách rất ngầm định sẽ tích lũy dần, và về lâu dài có thể dẫn tới một tình huống tương tự Visual Studio. Tôi nghĩ đây là một điểm hoàn toàn đáng lo. Tuy nhiên, như một bình luận khác đã nói, RAII cũng mang lại sự an toàn mà nó cung cấp, nên tôi cho rằng đây phần nào là một sự đánh đổi.
Cả hai bên đều đang nói đúng.
Ví von thì cũng giống như Azir trong game online Liên Minh Huyền Thoại thường được xem là một tướng bậc cao có khả năng split push, kiểm soát khu vực trong giao tranh tổng, và giá trị chiêu cuối cực kỳ vượt trội, nhưng đó là câu chuyện chỉ đúng trong các trận đấu chuyên nghiệp với trình độ rất cao; còn ở mặt bằng người chơi bình thường thì đi đường quá yếu, ngưỡng sức mạnh tổng thể cũng thấp, nên rốt cuộc chỉ là một vị tướng thuộc tier thấp nhất mà thôi.
Từ góc nhìn của những người có kiến thức về lập trình và hệ điều hành thuộc top 10% trở lên như Asahi Lina, thì dĩ nhiên các phương án ngoài RAII sẽ tốt hơn; nhưng ở phần việc mà 90% còn lại phải xử lý, tôi nghĩ không có gì bằng RAII hoặc Rust.
Tuy vậy, vì một trong những lý do lớn khiến phải bảo đảm tính ổn định/an toàn bộ nhớ là vấn đề bảo mật... nên tôi cho rằng trade-off là điều không thể tránh khỏi.
Nếu không có RAII thì những lập trình viên còn tương đối ít kinh nghiệm có vẻ sẽ tạo ra hàng loạt bug
Ít nhất ở cấp độ ứng dụng chứ không phải OS thì...