1 điểm bởi GN⁺ 2025-06-08 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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

 
GN⁺ 2025-06-08
Ý kiến trên Hacker News
  • Điều tôi thấy thú vị nhất ở zig là sự đơn giản của hệ thống build, khả năng cross-compile, và việc theo đuổi tốc độ lặp lại cao. Tôi là lập trình viên game nên hiệu năng rất quan trọng, nhưng với phần lớn yêu cầu thì hầu hết ngôn ngữ đều cho hiệu năng đủ tốt. Vì vậy đó không phải tiêu chí ưu tiên số một khi chọn ngôn ngữ. Có thể viết code mạnh mẽ bằng bất kỳ ngôn ngữ nào, nhưng tôi hướng tới các framework có tính tương lai và có thể bảo trì hàng chục năm. C/C++ trở thành lựa chọn mặc định vì được hỗ trợ ở khắp nơi, nhưng tôi cảm thấy zig cũng có thể sớm bắt kịp mức đó
    • Tôi từng thử chạy zig trên một thiết bị Kindle rất cũ (Linux 4.1.15) cho vui, và đã có một trải nghiệm khiến tôi ngạc nhiên về độ hoàn thiện của zig. Hầu hết mọi thứ hoạt động ngay lập tức, và tôi còn có thể debug những lỗi kỳ quặc bằng cả GDB cũ. Tôi cũng bị zig cuốn hút. Có thể xem trải nghiệm chi tiết tại đây
    • Tôi cũng cảm thấy có thể viết code mạnh mẽ bằng hầu hết ngôn ngữ, nhưng tôi muốn code mô-đun có thể nhìn xa hàng chục năm. Tôi thích Zig, nhưng nghĩ rằng nó có nhược điểm về bảo trì dài hạn và tính mô-đun. Zig là ngôn ngữ có phần đối nghịch với đóng gói. Không thể đặt thành viên của struct là private. Bình luận trong issue này là một ví dụ. Lập trường của Zig là không nên tồn tại cái gọi là biểu diễn nội bộ riêng, và mọi người dùng đều phải có thể biết phần hiện thực bên trong thông qua tài liệu/công khai. Nhưng để giữ được hợp đồng API, tức cốt lõi của phần mềm mô-đun, thì phải có khả năng che giấu hiện thực nội bộ, và điều đó hiện không làm được. Tôi hy vọng một ngày nào đó Zig sẽ hỗ trợ private field
    • Tôi đã dùng thử Rust một cách nhẹ nhàng và thấy thích. Nhưng vì nghe nói nó “tệ” nên tôi dừng một thời gian rồi giờ đang dùng lại. Vẫn thấy tốt. Tôi thật sự không hiểu vì sao mọi người lại ghét nó đến vậy. Cú pháp generic xấu xí thì C# và Typescript cũng thế. Borrow Checker cũng khá dễ hiểu nếu đã có kinh nghiệm với ngôn ngữ low-level
    • Zig cho cảm giác như một Rust đơn giản hơn, và là một Go tốt hơn. Mặt khác, trong số các công cụ được xây trên zig, tôi cực kỳ ấn tượng với bun. bun đã khiến cuộc sống của tôi tiện hơn rất nhiều. uv dựa trên Rust cũng mang lại trải nghiệm tương tự
    • Tôi đồng ý rằng C/C++ là mặc định. Hễ thử tạo ra thứ gì đó tốt hơn C thì đa số cuối cùng lại biến thành C++. Dù vậy, ta vẫn không nên ngừng thử. Rust và Zig là bằng chứng rằng vẫn còn lý do để kỳ vọng vào thứ tốt hơn. Từ giờ tôi định sẽ học C++ nhiều hơn
  • Dù đôi khi các compiler tối tân có thể phá vỡ đặc tả ngôn ngữ, giả định của Clang rằng vòng lặp vô hạn sẽ kết thúc thực ra là đúng theo tiêu chuẩn từ C11 trở đi. C11 nêu rõ như sau: "Compiler có thể giả định rằng một vòng lặp sẽ kết thúc nếu biểu thức điều khiển không phải constant expression và vòng lặp đó không thực hiện I/O, volatile, sync hay atomic operation"
    • Trong C++ thì quy định đó áp dụng cho mọi vòng lặp (ít nhất cho đến trước C++26), nhưng đúng như bạn nói, trong C thì nó chỉ áp dụng với “vòng lặp có biểu thức điều khiển không phải constant expression”. Tức là một vòng lặp vô hạn rõ ràng như 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 đề
  • Ngay cả khi không có tính năng 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 quan
    • Nhận xét đó đúng! Ví dụ ban đầu quá đơn giản. Một ví dụ tốt hơn là suffix automaton ở compile-time. Ngoài ra, đoạn code godbolt được link ở trên thực ra lại đang cho thấy một trong hai trường hợp không nên làm
  • Tôi không nghĩ phần ví dụ nói bytecode do V8 sinh ra cho đoạn JavaScript là kém hiệu quả là một ví dụ so sánh hay. Với Zig và Rust thì lại yêu cầu compile nhắm đến môi trường rất mới, trong khi với V8 thì không ép các tùy chọn tối ưu kiểu đó. Thực tế các JIT hiện đại cũng có thể vectorize nếu hoàn cảnh cho phép. Và đa số ngôn ngữ hiện đại cũng xử lý tối ưu liên quan đến chuỗi theo cách tương tự. Nhân tiện, cũng có ví dụ bằng C++
    • Về cơ bản, so sánh JS với Zig giống như so táo với salad trái cây. Ví dụ Zig dùng mảng có kiểu và kích thước cố định, còn JS là code “generic” nơi nhiều kiểu khác nhau có thể đi vào lúc runtime. Vì vậy trong JS, chỉ cần cung cấp tốt thông tin kiểu thì JIT có thể tạo ra vòng lặp nhanh hơn rất nhiều, dù chưa chắc vectorize được. Trên thực tế người ta không thường xuyên dùng TypedArray, 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ểu i < 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ỏ
    • Có thể đổi target trong các ví dụ godbolt của Rust và Zig sang CPU cũ hơn. Tôi đã không nghĩ đến giới hạn target ở phía JS. Và ví dụ C++ thì cho thấy clang sinh mã tốt đến mức nào. Tuy vậy, ngay lúc này phần assembly vẫn chưa thật sự khiến tôi hài lòng (ngay cả khi tính đến việc zig được build cho target CPU cụ thể). Nếu có ví dụ port suffix automaton ở compile-time sang C++ thì sẽ cực kỳ thú vị. Đây là một trường hợp sử dụng comptime thực tế mà compiler C++ khó lòng tự suy ra được
  • Tôi nghi ngờ nhận định rằng “ngôn ngữ high-level thiếu ‘intent’ mà ngôn ngữ low-level có”. Ngược lại, tôi cho rằng ưu điểm của ngôn ngữ high-level là có thể biểu đạt ý định chi tiết hơn bằng nhiều cách hơn
    • Tôi cũng đồng ý. Về bản chất, khác biệt giữa ngôn ngữ high-level và low-level là: trong ngôn ngữ high-level, ta biểu đạt ý định; còn trong low-level, ta phải phơi bày chính cơ chế hiện thực
    • Ở đây “ý định” không phải là ý định nghiệp vụ kiểu “tính thuế cho giao dịch này”, mà gần hơn với việc nói cho máy tính biết cần làm gì, ví dụ “dịch byte này sang trái ba bit”. Chẳng hạn, code như purchase.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
  • Tôi thật sự thích mô hình allocator của Zig. Giá mà trong Go có thể dùng thứ như allocator theo từng request thay vì GC thì hay biết mấy
    • Trong Go không phải là hoàn toàn không thể dùng custom allocator hay arena, nhưng tính thực dụng rất thấp và rất khó dùng cho đúng. Cũng không có cách nào ở cấp độ ngôn ngữ để biểu đạt hay ép buộc quy tắc ownership. Cuối cùng nó chỉ thành đang dùng C với cú pháp hơi khác, và nếu không có GC thì còn nguy hiểm hơn cả C++
  • Tôi đồng cảm với câu “tôi thích sự dài dòng của Zig”, nhưng thành thật mà nói thì sắc thái câu đó hơi lạ. C thì lỏng lẻo ở nhiều chỗ, còn Zig thì ngược lại, thường yêu cầu quá nhiều “annotation noise” (đặc biệt là khi phải cast số nguyên tường minh trong biểu thức toán học). Xem bài viết liên quan. Về hiệu năng, những trường hợp zig nhanh hơn c chủ yếu là vì Zig dùng cấu hình tối ưu LLVM mạnh tay hơn (-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ư unreachable thô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ữa comptime củ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à LLVM
    • Nếu nói về ví dụ phần cast, thì ngược lại, có thể tạo hẳn một hàm để bọc việc cast, nhờ vậy tăng khả năng tái sử dụng code và thể hiện ý định rõ hơn
      fn signExtendCast(comptime T: type, x: anytype) T {
        const ST = std.meta.Int(.signed, @bitSizeOf(T));
        const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
        return @bitCast(@as(ST, @as(SX, @bitCast(x))));
      }
      export fn addi8(addr: u16, offset: u8) u16 {
        return addr +% signExtendCast(u16, offset);
      }
      
      Cách này cũng sinh ra cùng assembly, lại hữu dụng và rõ ràng hơn
    • Các ý tưởng của Zig rất thú vị, và có vẻ trọng tâm thực tế nghiêng nhiều hơn về comptime và 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àn
    • Nếu xét đến việc dùng AI trong tương lai, tôi nghĩ các ngôn ngữ ngày càng tường minh và dài dòng sẽ trở nên phổ biến hơn. Bất kể có code bằng AI hay việc đó có đúng hay không, nhiều lập trình viên sẽ thích sự hỗ trợ từ AI, và ngôn ngữ cũng sẽ thay đổi theo điều đó
    • Nếu backend x86 mới được đưa vào, có lẽ về sau ta sẽ thấy những trường hợp chênh lệch hiệu năng giữa C và Zig thực sự đến từ chính dự án Zig
    • Về việc cast integer tường minh, sắp tới có thể sẽ có một cải tiến giúp cú pháp gọn gàng hơn. Xem thảo luận liên quan
  • Dù việc benchmark kiểu “C nhanh hơn Python” như thể so sánh bản thân ngôn ngữ là không đúng, nhưng một số tính năng của ngôn ngữ thực sự có thể trở thành rào cản lớn cho tối ưu. Nếu dùng ngôn ngữ phù hợp, cả lập trình viên lẫn compiler đều có thể biểu đạt ý định theo cách tự nhiên và nhanh hơn
  • Cú pháp for loop 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ờ
    • Mẫu duyệt hai mảng như thế này cực kỳ phổ biến trong code low-level, và việc duyệt song song cũng vậy. Vì thế việc Zig hỗ trợ điều đó một cách rõ ràng và tự nhiên lại là điều hợp lý. Tôi tò mò vì sao điều đó lại gây đau mắt đến vậy
  • Tối ưu hóa rất quan trọng. Tác động của nó càng về sau càng lớn hơn
    • Tuy vậy, điều đó chỉ đúng với giả định là phần mềm ấy thực sự được dùng đến