1 điểm bởi GN⁺ 5 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Nhánh master của Zig đã hợp nhất cải tiến xử lý số nguyên không theo kích thước ABI trong backend LLVM cùng ngữ nghĩa @bitCast mới, xử lý đồng thời các vấn đề tối ưu hóa và sự không nhất quán trong hành vi ngôn ngữ
  • Các số nguyên có độ rộng bit tùy ý như u4, i13, u40 giờ vẫn được xử lý như bit-int ở giá trị SSA, nhưng khi lưu vào bộ nhớ sẽ được mở rộng thành số nguyên có kích thước ABI
  • @bitCast trước đây gần với việc diễn giải lại byte trong bộ nhớ, nhưng định nghĩa mới diễn giải dựa trên mảng bit logic của kiểu, giúp giảm phụ thuộc vào endian
  • Thay đổi này đã được mở rộng tới backend LLVM·C và cả thực thi comptime, đồng thời các chỗ sử dụng liên quan trong thư viện chuẩn, trình biên dịch và compiler_rt cũng được rà soát
  • Khi các tối ưu hóa LLVM từng bị bỏ lỡ được khôi phục, trình biên dịch Zig ghi nhận cải thiện hiệu năng khoảng 5%, và có thể kỳ vọng một số cải thiện hiệu năng runtime trong 0.17.0

Thay đổi trong xử lý số nguyên có độ rộng bit tùy ý của backend LLVM

  • Trước đây, Zig hạ trực tiếp các kiểu số nguyên có độ rộng bit tùy ý như u4, i13, u40 xuống các kiểu bit-int của LLVM IR như i4, i13, i40
  • Cách làm này khiến ngữ nghĩa biểu diễn bộ nhớ của LLVM tạo ra những ràng buộc không cần thiết cho bộ tối ưu hóa, và vì Clang không tạo LLVM IR như vậy nên các nhánh xử lý bên trong LLVM cũng không được kiểm thử đầy đủ
  • Trong vài năm qua, đã thực sự quan sát thấy các trường hợp bỏ lỡ tối ưu hóabiên dịch sai
  • Cách làm mới vẫn giữ kiểu bit-int cho thao tác trên giá trị SSA, nhưng khi lưu vào bộ nhớ sẽ zero-extend hoặc sign-extend sang các kiểu có kích thước ABI như i8, i16, i32
  • Kiểu lowering này khớp với cách Clang lowering _BitInt(N) của C, nên được kỳ vọng sẽ đi theo nhánh được LLVM hỗ trợ tốt hơn

Giới hạn của @bitCast trước đây

  • Về mặt khái niệm, @bitCast trước đây gần với hành vi sau
    • Lấy con trỏ tới giá trị toán hạng
    • Ép kiểu con trỏ đó sang con trỏ của kiểu đích
    • Tải giá trị từ con trỏ đó
  • Tức là định nghĩa cũ gần với việc diễn giải lại byte trong bộ nhớ hơn là cấu trúc logic của kiểu
  • Theo thời gian, hành vi thực tế đã lệch khỏi định nghĩa này, và dù trên hầu hết target @sizeOf(u24) lớn hơn @sizeOf([3]u8), việc @bitCast từ [3]u8 sang u24 vẫn được cho phép
  • Backend LLVM đã hiện thực ngữ nghĩa @bitCast chưa được đặc tả đầy đủ, và khi thay đổi cách lưu số nguyên vào bộ nhớ, bộ kiểm thử của trình biên dịch đã xuất hiện Illegal Behavior và crash
  • Thay vì thêm logic vào backend LLVM để bắt chước hành vi cũ, hướng được chọn là triển khai toàn diện định nghĩa @bitCast mới

Ngữ nghĩa @bitCast mới

  • Ngữ nghĩa mới dựa trên đề xuất ngôn ngữ #19755 được gửi và chấp nhận trong năm 2024
  • Ngữ nghĩa này đã được hiện thực trong backend self-hosted x86_64, và với thay đổi lần này đã được mở rộng tới backend LLVM·C và cả thực thi comptime
  • @bitCast mới hoạt động dựa trên thứ tự bit biểu diễn logic của kiểu, chứ không phải byte trong bộ nhớ
    • u5 gồm 5 bit logic từ least-significant bit đến most-significant bit
    • [2]u5 gồm 10 bit logic, trong đó 5 bit của phần tử đầu tiên nối tiếp 5 bit của phần tử thứ hai
  • Với các chuyển đổi đơn giản giữa số nguyên, như đổi u8 sang i8 cùng kích thước, các bit được giữ nguyên và bit cao nhất được diễn giải như bit dấu
  • Ngữ nghĩa @bitCast giữa kiểu số nguyên và packed struct hoặc packed union cũng được giữ nguyên

Hành vi thay đổi với mảng·vector

  • Điểm mà ngữ nghĩa mới khác với trước đây là khi có các kiểu aggregate như mảng và vector tham gia
  • Ví dụ, nếu @bitCast từ [2]u8 sang u16, thì theo ngữ nghĩa cũ kết quả sẽ khác nhau tùy endian của target
    • Trên target big-endian, phần tử đầu tiên của mảng trở thành 8 bit cao
    • Trên target little-endian, phần tử đầu tiên của mảng trở thành 8 bit thấp
  • Ngữ nghĩa mới chỉ xét biểu diễn bit logic nên độc lập với endian, và trên mọi target phần tử đầu tiên của mảng sẽ trở thành 8 bit thấp
  • Nói chung, điều này gần với hành vi cũ trên target little-endian hơn
  • Cũng có thể thực hiện các chuyển đổi không điển hình như từ [2]u3 sang @Vector(3, u2)
    • Nối các bit logic của mảng lại rồi đọc theo đơn vị 2 bit để tạo thành các phần tử vector
    • Điều này cũng có thể dùng để @bitCast một số nguyên sang @Vector(n, u1) nhằm tách thành vector các bit riêng lẻ

Các đề xuất được áp dụng cùng lúc và quá trình di trú

  • Trong quá trình này, một số đề xuất nhỏ liên quan tới @bitCast cũng được hiện thực cùng lúc
    • Cấm @bitCast với vector con trỏ: #18936
    • Cho phép @bitCast với enum: một phần của #35602
  • Vì ngữ nghĩa mới khác biệt đáng kể so với cũ, các chỗ dùng @bitCast trong thư viện chuẩn, trình biên dịch và các thư viện hỗ trợ như compiler_rt đã được rà soát
  • PR liên quan là codeberg.org/ziglang/zig/pulls/35711, và khi được hợp nhất vào master, nhiều issue cũng đã được đóng cùng lúc
  • Ngữ nghĩa đã thay đổi và quy trình di trú được khuyến nghị sẽ được tổng hợp trong ghi chú phát hành Zig 0.17.0

Hiệu quả hiệu năng có thể kỳ vọng trong 0.17.0

  • Mục tiêu ban đầu là thay đổi lowering số nguyên không theo kích thước ABI trong backend LLVM, và việc này đã thành công trong việc khôi phục các tối ưu hóa từng bị bỏ lỡ
  • Kết quả liên quan có thể xem tại demonstrably successful
  • Bản thân trình biên dịch Zig, dù nội bộ không dùng quá nhiều số nguyên có độ rộng bit tùy ý, vẫn cho thấy cải thiện hiệu năng khoảng 5% nhờ tối ưu hóa tốt hơn
  • Trong 0.17.0, một số đoạn mã có thể nhận được cải thiện nhỏ về hiệu năng runtime

1 bình luận

 
Các ý kiến trên Lobste.rs
  • Bài viết nói rằng biểu diễn bit logic là độc lập với endian, nhưng phần giải thích thực tế trông rõ ràng là theo kiểu little-endian, không hỗ trợ thứ tự bit hay thứ tự byte big-endian

    • Có vẻ độc lập với endian ở đây nghĩa là hành vi không thay đổi giữa các kiến trúc little-endian/big-endian
  • Đây là nhật ký phát triển mới ngày 25/6/2026, nói rằng ngữ nghĩa @bitCast mới và các cải tiến backend LLVM đã được hợp nhất trong pull request gần đây

  • Khá thú vị, nhưng tôi tự hỏi liệu trên các đích big-endian vốn hiếm khi được kiểm thử, những đoạn mã viết như dưới đây có thể đột ngột bị hỏng không
    Viết bằng mã giả không phải Zig:

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • Tôi cũng đã nghĩ vậy, nhưng rốt cuộc nếu trì hoãn một thay đổi không thể tránh khỏi thì vấn đề chỉ lớn hơn mà thôi
      Thực ra có lẽ không phải vấn đề lớn; trong số hàng nghìn @bitCast trong kho Zig, hình như số chỗ bị ảnh hưởng bởi thay đổi này ít hơn 100 rất nhiều
      Thành thật mà nói, tôi cũng không nghĩ phần lớn người dùng Zig thật sự biết chính xác @bitCast hoạt động thế nào khi chuyển đổi giữa mảng/vector và scalar. Trước đây nhiều đoạn mã chỉ được kiểm thử trên hệ thống của tác giả và chỉ chạy đúng trên little-endian, còn giờ có lẽ sẽ chạy được ở mọi nơi
  • Với tư cách một lập trình viên C đời cũ, tôi nhớ rằng bit field của C không mấy được ưa chuộng vì hành vi của chúng không khả chuyển giữa các kiến trúc
    Ngữ nghĩa @bitCast mới của Zig là ngữ nghĩa trừu tượng khả chuyển, cho cùng kết quả trên các kiến trúc khác nhau, nên tôi nghĩ đây đúng là hướng cần thiết
    Gần đây tôi đang thiết kế bit field và bit cast cho ngôn ngữ của mình, nên tôi định xem kỹ hơn tài liệu thiết kế và triển khai của Zig để làm rõ mã của tôi nên hoạt động như thế nào

    • Phương án thay thế chính của Zig cho bit field trong C có lẽ là packed structpacked union, và cả hai đều được định nghĩa để khớp tốt với định nghĩa @bitCast mới
      packed struct hoạt động bằng cách nhét các bit của trường vào “số nguyên nền”. Ví dụ nếu các trường là bool, u6, i9 và số nguyên nền là u16, thì bit thấp nhất của u16bool, 6 bit tiếp theo là u6, và 9 bit còn lại là i9. Nói cách khác, packed struct của Zig gần như là cú pháp tiện dụng đặt trên nhiều phép shift và mask
      packed union cũng có số nguyên nền, nhưng mọi trường phải dùng đúng cùng số bit với số nguyên nền. Vì vậy việc ghi vào một trường rồi đọc từ trường khác gần như tương đương với @bitCast theo ngữ nghĩa mới. Tuy nhiên các trường của packed union/packed struct không thể có kiểu mảng hoặc vector
      Cá nhân tôi thấy các công cụ này rất phù hợp để biểu diễn “cấu trúc liên quan đến bit”. Có thể đóng gói nhiều giá trị bằng packed struct để dùng như bit field trong C, và vì đây là cú pháp tiện dụng trên các phép toán bit, nên cả bit flag vốn trong C thường phải xử lý bằng một đống macro không an toàn kiểu cũng có thể được biểu diễn gọn gàng
      Ví dụ, với các cờ truy cập RWX, trong C có thể nhận qua các macro ACCESS_READ, ACCESS_WRITE, ACCESS_EXEC và API uint8_t, còn trong Zig có thể định nghĩa Access = packed struct(u8) với các trường read, write, exec, reserved và để API nhận Access
      Dùng packed structpacked union cũng có thể biểu diễn những bố trí bit khá kỳ lạ. Trong entry bảng ký hiệu của định dạng đối tượng Mach-O có trường n_type đặc biệt, có vẻ do lý do lịch sử; có thể mô hình hóa nó dưới dạng bits: packed struct(u8)stab: enum(u8) bên trong packed union(u8)
      Khi xử lý giá trị n_type này, không cần shift hay mask thủ công. Chỉ cần kiểm tra n_type.bits.is_stab != 0, nếu đúng thì switch trên n_type.stab, còn nếu không thì xem các trường khác của n_type.bits. Ngược lại, cũng có thể tạo giá trị như .{ .stab = .gsym } hoặc .{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }
      Hơi dài dòng về một tính năng ngôn ngữ khác với chủ đề bài gốc, nhưng nếu bạn đang tìm thứ có thể tham khảo cho thiết kế ngôn ngữ mới, thì nên trực tiếp thử packed structpacked union của Zig. Chúng đơn giản nhưng tôi nghĩ là những công cụ khá tốt