1 điểm bởi GN⁺ 2 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Crate tạo số ngẫu nhiên tiêu biểu của Rust, rand, có các thao tác thường dùng nằm rải rác ở nhiều trait, nên urandom được phát triển với bề mặt API công khai/triển khai nhỏ hơn và trải nghiệm sử dụng nhất quán hơn
  • Gom các thao tác cấp cao vào một cấu trúc Random và niêm phong trait Rng, ưu tiên khả năng khám phá API và tối ưu hóa nội bộ hơn là hỗ trợ generator tùy ý
  • Không đưa vào thuật toán tạo số ngẫu nhiên mới; thay vào đó chọn hàm xuất của Xoshiro256 theo từng mục đích, đạt thông lượng cao hơn khoảng 31% so với rand 0.10.2 trong benchmark tạo 1.000 giá trị f64
  • Lấy mẫu số nguyên phân bố đều đã hợp nhất đường dẫn tái sử dụng và dùng một lần bằng một triển khai không thiên lệch duy nhất với ngưỡng được tính trễ; trong benchmark phạm vi 500..20_000, nó nhanh hơn cả hai đường dẫn của rand
  • Đầu ra thô với seed tường minh được bảo đảm tái lập trên các kiến trúc được hỗ trợ và các bản phát hành tương thích SemVer, nhưng phải đánh đổi việc kết nối generator tùy ý cùng hệ sinh thái phân phối và tích hợp bên thứ ba rộng lớn của rand

API Random được gom về một nơi

  • Các thao tác hữu ích của rand nằm rải rác ở nhiều trait
    • Tạo số ngẫu nhiên trong phạm vi cần RngExt, chọn từ sequence cần IndexedRandom, xáo trộn cần SliceRandom
    • rand 0.10 cung cấp các helper ở cấp root như rand::random_range cho lệnh gọi dùng một lần
    • Nếu muốn giữ handle RNG hoặc dùng các thao tác trên sequence như chọn/xáo trộn, vẫn phải tìm các method thuộc nhiều trait
  • Dù prelude giảm bớt import, bạn vẫn phải biết extension method áp dụng cho kiểu nào trong RNG, slice hay iterator, nên khó tìm chỉ bằng tự động hoàn thành của IDE
  • urandom đặt API tiêu thụ cấp cao vào một cấu trúc wrapper Random duy nhất
    • Tạo Random<urandom::rng::Xoshiro256Rng> bằng urandom::new()
    • Có thể gọi uniform, choose, shuffle trên cùng một đối tượng
    • Tự động hoàn thành có thể hiển thị random, uniform, chance, choose, shuffle, sample, v.v.
    • Tất cả đều là method riêng có sẵn, nên không cần tìm hay import trait mở rộng cấp cao

Trait Rng được niêm phong, chọn tối ưu hóa thay vì khả năng mở rộng

  • rand xem trait RNG cấp thấp là điểm mở rộng công khai, nhưng trait Rng của urandom được niêm phong, để crate tự chọn và triển khai các generator được hỗ trợ
    • Không thể gắn generator tùy ý vào Random
    • Muốn thêm generator mới thì phải thay đổi chính urandom
  • Nếu mục tiêu là thuật toán tốt hơn, các lựa chọn mặc định theo vai trò hiện nay đã là Xoshiro256 và ChaCha, và khuyến nghị cũng thay đổi chậm
    • Nếu có lựa chọn tốt hơn, có thể áp dụng trong một bản phát hành major trong tương lai
  • Để tương thích với dự án/ngôn ngữ lập trình/thuật toán legacy/phần cứng đặc thù/generator chuyên cho mô phỏng khác, chỉ giống generator là chưa đủ
    • Các thuật toán liên quan như lấy mẫu đều và xáo trộn cũng phải giống nhau, nên một triển khai chuyên biệt đáp ứng toàn bộ hợp đồng sẽ phù hợp hơn
  • Nhờ trait được niêm phong, có thể thêm đúng các thao tác thô mà urandom cần mà không phải thiết kế/tài liệu hóa hợp đồng triển khai cho các generator chưa biết và những tình huống ngoại lệ
    • Có thể chuyên biệt hóa generator và thuật toán để khớp nhau, cho phép một số tối ưu hóa không thể dùng trong rand
  • Với phần lớn ứng dụng, lựa chọn entropy hữu ích hơn việc triển khai PRNG mới
    • Các generator cụ thể công khai constructor from_seed native
    • Có thể tạo Random với seed tường minh như ChaCha12Rng::from_seed(seed)
    • Không nhận triển khai RNG tùy ý, nhưng vẫn giữ các điểm mở rộng mà người dùng nâng cao được dự đoán sẽ cần

Cải thiện hiệu năng từ cùng thuật toán

  • urandom không dùng thuật toán tạo số ngẫu nhiên mới
    • Trên hệ 64-bit, urandom::new() cho mục đích phi mật mã và rand::rngs::SmallRng dùng cùng họ Xoshiro256
    • urandom::csprng() cho mục đích mật mã và rand::rngs::StdRng dùng ChaCha12
    • Generator bên trong của hàm tiện ích rand::rng() cũng là ChaCha12
  • Giao diện generator của rand cung cấp word số nguyên và lấp đầy byte, nên ngay cả phân phối cần f64 cũng yêu cầu cả u64 trước
  • urandom::Rng cung cấp không chỉ next_u32, next_u64 mà cả next_f32next_f64
    • Số ngẫu nhiên dấu phẩy động cần ít bit ngẫu nhiên hơn một word đầy đủ
    • Generator có thể override các method này bằng hàm xuất rẻ hơn
  • Triển khai Xoshiro dùng chung chuyển đổi trạng thái nhưng tách đường xuất
    • Giữ Xoshiro256++ cho u64
    • Với u32 và dấu phẩy động, dùng Xoshiro256+ nhanh hơn, có các bit cao được thiết kế phù hợp mục đích đó
  • Kết quả microbenchmark tạo 1.000 số ngẫu nhiên bằng urandom 1.0 và rand 0.10.2 như sau
    • Xoshiro u64: cả hai đều 814ns
    • Xoshiro u32: rand 836ns, urandom 788ns
    • Xoshiro f64: rand 1.033ns, urandom 788ns
    • ChaCha12 f64: rand 2.199ns, urandom 2.011ns
  • Thông lượng đầu-cuối của Xoshiro f64 cao hơn khoảng 31% và thời gian chạy ngắn hơn 24%, nhưng đường dẫn u64 thực hiện cùng công việc thì gần như hòa
  • ChaCha12 không override next_f64, nên hiệu năng nhìn chung tương tự
  • Thời gian chính xác thay đổi theo máy và compiler; có thể xem điều kiện chi tiết trong ghi chú benchmark đầy đủ

Một đường dẫn lấy mẫu đều hợp nhất

  • Nếu chỉ lấy phần dư của số nguyên theo độ dài phạm vi, sẽ sinh ra thiên lệch, nên lấy mẫu số nguyên đều đúng cách phải loại bỏ một phần đầu ra của generator
  • Tính ngưỡng loại bỏ chính xác cần phép lấy dư tốn kém
    • Nếu sampler được dùng lặp lại, chi phí thiết lập ban đầu có thể chấp nhận được
    • Khi chỉ tạo một giá trị, chi phí này trở nên tương đối lớn
  • rand phơi bày khác biệt này qua trait UniformSampler
    • UniformInt được tạo ra tính trước ngưỡng để lấy mẫu không thiên lệch
    • Rng::random_range dùng hook sample_single hoặc sample_single_inclusive riêng để tránh thiết lập ban đầu
    • Ở tính năng mặc định, đường tắt dùng một lần sử dụng một thuật toán thứ hai hơi thiên lệch
    • Tính năng tùy chọn unbiased thay nó bằng một phiên bản lặp phức tạp hơn
  • urandom tính trễ ngưỡng và dùng một triển khai nhân–loại bỏ không thiên lệch duy nhất cho cả phạm vi tái sử dụng lẫn dùng một lần
    • Theo phương pháp được mô tả trong bài báo năm 2018 của Daniel Lemire, Fast Random Integer Generation in an Interval
    • Với hầu hết phạm vi thực tế, ứng viên đầu tiên được trả về trước khi cần phép chia
    • Nếu không thể trả về ứng viên đầu tiên, ngưỡng chính xác được tính rồi lặp lại không thiên lệch
    • Cũng xử lý ngoại lệ range == 0 khi yêu cầu toàn bộ phạm vi
  • Cùng một triển khai xử lý phân phối tái sử dụng và phạm vi dùng một lần mà không cần method riêng, thuật toán thứ hai, chi phí thiết lập trước hay đường nhanh thiên lệch
  • Kết quả benchmark lấy 1.000 mẫu trong phạm vi 500..20_000 như sau
    • UniformInt tái sử dụng: rand 1.098ns, urandom 950ns
    • Phạm vi dùng một lần: rand 1.079ns, urandom 942ns
  • Kết quả của rand dựa trên tính năng mặc định, nên dòng dùng một lần nhanh hơn là đường hơi thiên lệch, trong khi urandom không thiên lệch mà vẫn nhanh hơn cả hai đường dẫn

Khả năng tái lập xuyên suốt bản phát hành và kiến trúc

  • urandom xem khả năng tái lập là một phần của hợp đồng công khai
    • Với cùng seed tường minh và cùng thứ tự gọi RNG cấp thấp, đầu ra thô của generator quyết định luận được giữ nguyên
    • Bảo đảm ổn định trên các kiến trúc được hỗ trợ và trong toàn bộ các bản phát hành tương thích SemVer
    • Server 64-bit và client WebAssembly 32-bit có thể dùng cùng nền tảng generator để phát lại
  • Để duy trì tương thích này, hiệu năng trên kiến trúc 32-bit bị đánh đổi
  • Đây là bảo đảm mạnh hơn chính sách tái lập của rand
    • Các generator portable và thuật toán lấy mẫu của rand có thể thay đổi đầu ra trong bản phát hành minor
    • SmallRngStdRng được nêu rõ là không portable và cũng có thể thay đổi theo nền tảng hoặc bản phát hành thư viện

Cái giá của lựa chọn và tiêu chí áp dụng

  • urandom gom các thao tác phổ biến vào Random để dễ tìm mà không cần trait mở rộng
  • Thiết kế generator và phân phối cùng nhau để triển khai đường xuất Xoshiro rẻ hơn và một đường lấy mẫu đều không thiên lệch duy nhất
  • Dòng thô ổn định của generator được seed tường minh có thể dùng cho game và mô phỏng quyết định luận
  • Đổi lại, không thể mang generator tùy ý vào, và cũng không có danh sách phân phối lớn hơn cùng hệ sinh thái tích hợp bên thứ ba mà rand cung cấp
  • Nếu cần hệ sinh thái rộng, rand là lựa chọn phù hợp; nếu ưu tiên bề mặt API nhỏ, khả năng khám phá, tối ưu hóa tích hợp và chính sách tái lập mạnh, có thể chọn urandom
  • Có thể xem package tại crates.io, tài liệu APImã nguồn GitHub

1 bình luận

 
Ý kiến trên Lobste.rs
  • Có đủ lý do để fork rand, nhưng cái tên urandom lại nghe như một thư viện liên quan đến /dev/urandom

    • Có vẻ hữu ích, nhưng cái tên có thể gây nhầm lẫn. Nếu chỉ nhìn tên mà chưa đọc bài, có lẽ tôi sẽ nghĩ nó phụ thuộc vào file I/O nên đã không tìm hiểu
  • Tôi đồng ý với vấn đề được nêu ra, nhưng không thích pub fn new() -> Random<impl Rng + Clone>
    Nếu tham số hóa toàn bộ ứng dụng thành Random<T> where T: Rng thì sẽ phát sinh thêm nhiều việc phiền phức, và các vấn đề về thời gian biên dịch cùng dyn sẽ trở nên nghiêm trọng. Tôi thà để struct Random có kiểu cụ thể, hoặc phương án thứ hai là chọn struct Random<T = rng::Xoshiro256Rng>

  • Vì cảm giác bức bối tương tự nên tôi cũng từng tự làm một cái, nhưng không phải bản fork, và ít tính năng hơn rand rất nhiều

  • Thật vui khi thấy có người gặp vấn đề giống tôi và thực sự bắt tay vào giải quyết. Có vẻ Rust kỳ lạ ở chỗ có xu hướng khiến người ta tạo ra các thư viện “súp trait”
    Kiểu dữ liệu cốt lõi của cơ sở dữ liệu tôi làm ở công ty phải implement ít nhất 15 trait, nên tự động hoàn thành trở nên lộn xộn và tài liệu cũng rối rắm. Tôi đã giảm bớt được một phần số trait, nhưng thường bị chặn bởi các vấn đề như phụ thuộc vòng hoặc không thể viết các bài kiểm thử cốt lõi

    • Đây là hiện tượng do những “phi hành gia kiến trúc” xuất thân từ Java áp dụng cùng kiểu hướng đối tượng đó vào Rust. Phụ thuộc vòng là dấu hiệu cho thấy bạn đã cố tách một thứ vốn chưa thể tách, hoặc chưa phân biệt đúng ba đối tượng. Nếu bạn có thể kiểm soát toàn bộ mã, hãy dùng enum thay vì trait
    • Trong hệ sinh thái mật mã Rust, vấn đề súp trait đặc biệt nghiêm trọng đến mức phát điên
  • Thư viện này khiến tôi nhớ одновременно đến giao diện sâu của APOSD và công việc mật mã của Filippo được thiết kế để khó dùng sai, và cả hai đều là lời khen rất lớn

    • Tuy vậy, vì urandom::new() không trả về trình tạo số ngẫu nhiên an toàn về mặt mật mã, nên đây chưa phải một thiết kế hoàn toàn không thể dùng sai. Đặc biệt là vì /dev/urandom trên Linux lại an toàn, nên càng dễ gây nhầm lẫn
  • Một lựa chọn thay thế khác cho randfastrand, một trình tạo số ngẫu nhiên đơn giản và nhanh. Nó đơn giản hơn randurandom, nhưng cũng ít tính năng hơn