Tối ưu hóa mức thấp và Zig
(alloc.dev)- Tối ưu hóa mức thấp có thể được triển khai dễ dàng trong ngôn ngữ Zig
- Trình biên dịch trong đa số trường hợp thực hiện tối ưu hóa rất tốt, nhưng đôi khi cần truyền đạt rõ ràng ý đồ của lập trình viên để đạt hiệu năng tốt hơn
- Zig hỗ trợ sinh mã hiệu năng cao và metaprogramming mạnh mẽ bằng tính năng thực thi thời điểm biên dịch (comptime)
- Khi so với Rust, Zig cho phép tối ưu hóa chính xác hơn nhờ annotation và cấu trúc mã tường minh
- Trong các phép toán lặp lại như so sánh chuỗi, có thể tận dụng comptime để tạo ra mã assembly vượt trội hơn hàm thông thường
Tối ưu hóa và Zig
Như lời cảnh báo nổi tiếng: "Mọi thứ đều có thể, nhưng điều thú vị thì không dễ đạt được.", tối ưu hóa chương trình luôn là một mối quan tâm chính của nhà phát triển. Để giảm chi phí hạ tầng đám mây, cải thiện độ trễ, đơn giản hóa hệ thống, tối ưu hóa mã là điều bắt buộc. Bài viết này tập trung giải thích khái niệm tối ưu hóa mức thấp trong Zig và những điểm mạnh của Zig.
Có thể tin vào trình biên dịch không?
- Nói chung có rất nhiều lời khuyên kiểu "hãy tin vào trình biên dịch", nhưng trên thực tế vẫn có trường hợp trình biên dịch hoạt động khác kỳ vọng hoặc vi phạm đặc tả ngôn ngữ
- Ngôn ngữ cấp cao khó truyền đạt rõ ý đồ (intent), nên đi kèm những ràng buộc về hiệu năng
- Ngôn ngữ cấp thấp, nhờ tính tường minh của mã, cho phép trình biên dịch biết được thông tin cần thiết để tối ưu; ví dụ khi so sánh hàm maxArray trong JavaScript và Zig, Zig truyền đạt kiểu dữ liệu rõ ràng, căn chỉnh, khả năng alias hay không... ngay tại thời điểm biên dịch thay vì lúc chạy
- Nếu viết cùng phép toán maxArray bằng Zig và Rust, ta có thể nhận được mã assembly hiệu năng cao gần như giống nhau, nhưng càng biểu đạt tốt ý đồ thì kết quả tối ưu hóa càng được cải thiện
- Tuy nhiên không phải lúc nào cũng có thể hoàn toàn tin vào năng lực của trình biên dịch, nên ở các đoạn nghẽn cổ chai cần tự kiểm tra mã và kết quả biên dịch để tìm cách tối ưu
Vai trò của Zig
- Zig, nhờ các đặc tính như tính tường minh chính xác, hàm dựng sẵn phong phú, con trỏ và annotation, comptime, cùng Illegal Behavior được định nghĩa rõ ràng, có thể tạo ra mã đã tối ưu mà không cần thông tin trừu tượng
- Rust nhờ mô hình bộ nhớ mà mặc định đảm bảo không có alias giữa các đối số, còn trong Zig thì cần tự thêm annotation như noalias
- Nếu chỉ xét theo LLVM IR, mức độ tối ưu hóa của Zig cũng rất cao
- Trên hết, comptime (thực thi thời điểm biên dịch) của Zig là một công cụ tối ưu hóa cực kỳ mạnh
comptime là gì?
- comptime của Zig được dùng cho sinh mã, nhúng giá trị hằng, tạo struct generic dựa trên kiểu, và đóng vai trò quan trọng trong việc cải thiện hiệu năng lúc chạy
- Có thể triển khai metaprogramming bằng comptime
- Khác với macro của C/C++ hay hệ thống macro của Rust, comptime không phải cú pháp riêng mà là mã thông thường
- Mã comptime không trực tiếp sửa AST, nhưng có thể kiểm tra, phản ánh và sinh ra kết quả cho mọi kiểu ngay tại thời điểm biên dịch
- Tính linh hoạt của comptime cũng đã ảnh hưởng đến việc cải tiến các ngôn ngữ khác như Rust, và nó được tích hợp rất tự nhiên vào ngôn ngữ Zig
Giới hạn của comptime
- Một số tính năng macro như token-pasting không thể được thay thế bằng Zig comptime
- Zig coi trọng tính dễ đọc của mã, nên không cho phép tạo biến vượt ra ngoài phạm vi hay định nghĩa macro kiểu như vậy
- Thay vào đó, Zig comptime có rất nhiều ví dụ ứng dụng metaprogramming rộng rãi như phản chiếu kiểu, triển khai DSL, tối ưu hóa phân tích chuỗi, v.v.
Tối ưu hóa so sánh chuỗi bằng comptime
- Hàm so sánh chuỗi thông thường có thể được triển khai trong mọi ngôn ngữ, nhưng trong Zig, nếu một trong hai chuỗi là hằng số đã biết tại comptime thì có thể sinh ra mã assembly hiệu quả hơn
- Ví dụ, nếu một chuỗi luôn là
"Hello!\n", ta có thể áp dụng tối ưu hóa bằng cách so sánh theo khối lớn hơn thay vì từng byte - Để làm vậy, có thể dùng comptime để sinh mã hiệu năng cao ngay tại thời điểm biên dịch, bao gồm vector SIMD, xử lý theo khối, tối ưu hóa số byte còn lại, v.v.
- Bằng cách này, không chỉ các phép so sánh chuỗi lặp đi lặp lại mà cả nhiều loại mã thiên về hiệu năng như mapping dựa trên dữ liệu tĩnh, bảng băm hoàn hảo, AST parser, v.v. cũng có thể được triển khai
Kết luận
- Zig rất phù hợp với tối ưu hóa mức thấp, và nhờ cấu trúc mã tường minh cùng khả năng comptime mạnh mẽ, có thể trực tiếp hiện thực hiệu năng cao nhất
- Ngay cả khi so với các ngôn ngữ khác như Rust, năng lực lập trình thời điểm biên dịch và tính tường minh của Zig vẫn mang lại lợi thế lớn trong phát triển phần mềm hiệu năng cao
- Năng lực tối ưu hóa của Zig sẽ tiếp tục là một lợi thế cạnh tranh ngày càng quan trọng trong tương lai
1 bình luận
Ý kiến trên Hacker News
bun.bunđã khiến cuộc sống của tôi tiện hơn rất nhiều.uvdựa trên Rust cũng mang lại trải nghiệm tương tựfor(;;);thì thực sự phải là vô hạn, vàloop {}của Rust cũng vậy. Nhưng các lập trình viên LLVM đôi khi cứ tưởng họ chỉ đang làm compiler cho C++, nên dù Rust có nói “làm ơn tạo vòng lặp vô hạn”, LLVM vẫn tối ưu theo kiểu “theo C++ thì chuyện đó không tồn tại!”. Thành ra áp dụng sai tối ưu cho sai ngôn ngữ và gây ra vấn đềcomptime, việc inline và unroll so sánh chuỗi cũng hoàn toàn có thể làm được trong C. Ví dụ liên quanTypedArray, vì chi phí khởi tạo cao và chỉ đáng dùng khi tái sử dụng nhiều. Ngoài ra bài viết nói code JS bị phình ra, nhưng phần lớn là vì JIT không thể tin hoàn toàn vào kiểm tra độ dài mảng nên phải chèn guard; thực tế ai cũng viết vòng lặp kiểui < x.lengthđể JIT tối ưu. Theo nghĩa đó thì đây hơi giống bắt bẻ tiểu tiết, dù đúng là có khác biệt nhỏcomptimethực tế mà compiler C++ khó lòng tự suy ra đượcpurchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?;chứa đầy ý định, nhưng gần như không thể dự đoán máy sẽ sinh ra machine code thế nào-march=native, tối ưu toàn chương trình, v.v.). Thực ra trong C cũng có thể dùng các gợi ý tối ưu nhưunreachablethông qua extension của ngôn ngữ, và Clang cũng rất tích cực trong constant folding. Nói cách khác, khác biệt giữacomptimecủa Zig và codegen của C nhiều khi đến từ cấu hình tối ưu compiler. TL;DR: khi C chậm, trước tiên hãy kiểm tra cài đặt compiler. Dù sao thì cốt lõi của tối ưu vẫn là LLVMcomptimevà compile toàn chương trình so với điều tôi kỳ vọng từ bài gốc. Tôi đồng ý với điều đó. Nhân tiện, Virgil đã hỗ trợ sử dụng toàn bộ ngôn ngữ ở compile-time và compile toàn chương trình từ năm 2006. Virgil không target LLVM, nên so sánh tốc độ rốt cuộc là so sánh backend. Nhờ cách tiếp cận này, Virgil có thể tối ưu rất mạnh như devirtualize lời gọi method từ trước, loại bỏ tối đa field/object không dùng, constant propagation đến cả đối tượng trên heap gắn với field, và chuyên biệt hóa hoàn toànforloop của Zig khiến tôi thấy quá rối mắt. Phải đặt hai danh sách cạnh nhau rồi canh vị trí cho khớp, chỉ nhìn thôi đã đau mắt. Tôi cho rằng các ngôn ngữ gần đây mắc sai lầm khi nhồi vào quá nhiều cú pháp “ma thuật” và ký hiệu đặc biệt. Có lẽ tôi sẽ không chịu nổi nếu phải nhìn chằm chằm vào nó hàng giờ