我为什么 fork 了 rand

Lobsters Hottest 工具

摘要

作者解释了为什么将 Rust 的 rand crate fork 成 urandom,主要关注更小的 API 表面、更好的可发现性,以及密封的 Rng trait 来简化自定义随机数的使用。

<p><a href="https://lobste.rs/s/kvxee3/why_i_forked_rand">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/31 16:49

# Casper 的博客 —— 为什么我 fork 了 rand 2026 年 7 月 27 日 —— rust Rust 的 `rand` crate 是 Rust 生态中随机数处理的默认选择。然而,我在使用 `rand` 时遇到了一些令人沮丧的小问题:很多日常操作很难被发现!这促使我 fork 并构建了 `urandom`(https://crates.io/crates/urandom):一个公共 API 和实现面更小、围绕特定用户体验组织的随机数 crate。引用一句名言: > “完美不是无可增添,而是无可削减。” ## 一个查找的地方 使用 `rand` 时,有用的操作来自多个 trait: ```rust use rand::RngExt; use rand::seq::{IndexedRandom, SliceRandom}; let mut rng = rand::rng(); let roll: u32 = rng.random_range(1..=6); let color = ["red", "green", "blue"].choose(&mut rng); let mut numbers: Vec<_> = (1..=10).collect(); numbers.shuffle(&mut rng); ``` Rand 0.10 还提供了根级辅助函数,例如 `rand::random_range`,用于一次性调用。一旦你持有 RNG 句柄,或需要序列操作(如选择、洗牌),方法 API 仍然来自多个 trait。prelude 缩短了导入语句,但你仍然需要知道这些扩展方法适用于哪些类型。当方法可能属于 RNG、slice、迭代器或其他地方时,IDE 补全帮助不大。要找到正确的方法,通常需要事先知道哪个 trait 在哪个类型上提供了它。 `urandom` 将它的高级消费 API 放在一个 `Random` 包装结构体上: ```rust let mut rand = urandom::new(); // rand: urandom::Random let roll: u32 = rand.uniform(1..=6); let color = rand.choose(&["red", "green", "blue"]); let mut numbers: Vec<_> = (1..=10).collect(); rand.shuffle(&mut numbers); ``` 如果你有一个 `Random`,编辑器补全会向你展示 `random`、`uniform`、`chance`、`choose`、`shuffle`、`sample` 等更多方法。这些是固有方法(inherent methods),不需要查找或导入任何高级扩展 trait。 ## 密封的 `Rng` `rand` 将其底层 RNG trait 视为公共扩展点。`urandom` 则不这样:它的 `Rng` trait 是密封的(sealed),受支持的生成器在 crate 内部选择和实现。 - 你无法将任意生成器插入 `Random`。 - 新增生成器需要修改 `urandom`。 我反复思考一个核心问题:自定义生成器究竟能带来什么实质好处?如果目标是更好的算法,Xoshiro256 和 ChaCha 在当前已经是各自角色的既定默认选择,而且推荐变化很慢。如果这一点改变,`urandom` 可以在未来的主版本中采用更好的选择。如果目标是与其他项目、编程语言、遗留算法、专用硬件或仿真专用生成器兼容,那么仅仅匹配其生成器是不够的。均匀采样、洗牌等算法也必须匹配。对这种完整契约做专门实现才是更合适的做法。 密封 `Rng` 让我可以围绕自己的 crate 来设计它。我可以只添加 `urandom` 所需的原语,而不必为未知生成器及其特殊情况设计和记录实现契约。这让生成器和算法可以彼此特化,实现 `rand` 无法获得的针对性优化。 对于大多数应用,选择熵比实现新 PRNG 更有用。具体生成器暴露了它们原生的 `from_seed` 构造函数: ```rust fn from_my_entropy(seed: [u32; 8]) -> urandom::Random { urandom::rng::ChaCha12Rng::from_seed(seed) } ``` 这比接受任意 RNG 实现更窄,但它保留了高级用户可能需要的扩展点。 ## 没有更好的算法也能胜过 `rand` 我的 fork 并不是基于新颖的随机数算法。在 64 位系统上,`urandom::new()` 和 `rand::rngs::SmallRng` 在非加密用途中使用相同的 *Xoshiro256* 系列。`urandom::csprng()` 和 `rand::rngs::StdRng` 在加密用途中使用 *ChaCha12*。Rand 便捷的 `rand::rng()` 句柄由 ChaCha12 提供支持。`rand` 的生成器接口暴露的是整数词和字节填充。一个想要 `f64` 的分布一开始就会请求完整的 `u64`。`urandom::Rng` 还有专门的 `next_f32` 和 `next_f64`: ```rust pub trait Rng: Sealed { fn next_u32(&mut self) -> u32; fn next_u64(&mut self) -> u64; fn next_f32(&mut self) -> f32; fn next_f64(&mut self) -> f64; // byte filling omitted } ``` 随机浮点数所需的随机位比完整机器字少。因此生成器可以用更廉价的输出函数覆盖这些方法。Xoshiro 算法家族正好支持这一点。实现保留 Xoshiro256++ 用于 `u64`,而 `u32` 和浮点数使用更快的 Xoshiro256+。两者共享相同的状态推进,但 Xoshiro256+ 的输出函数更廉价,其高位正是为这种用例设计的。在我的系统上,将当前 `urandom` 1.0 实现与 `rand` 0.10.2 比较,端到端的 Xoshiro `f64` 基准测试在此微基准中测得的吞吐量大约高出 31%(快 24%)。在两者做相同工作的 `u64` 路径上,结果基本持平。请参阅完整的基准测试说明(https://github.com/CasualX/urandom/blob/master/docs/benchmarks-rand.md)了解更多。 1,000 个随机值: | 输出 | `rand` | `urandom` | | --- | --- | --- | | Xoshiro `u64` | 814 ns | 814 ns | | Xoshiro `u32` | 836 ns | 788 ns | | Xoshiro `f64` | 1,033 ns | 788 ns | | ChaCha12 `f64` | 2,199 ns | 2,011 ns | 确切的计时取决于机器和编译器。在这里,更丰富的接口让 Xoshiro 能为 `u32` 和浮点数选择更廉价的输出函数。ChaCha12 并没有覆盖 `next_f64`;因此性能相当。 ## 统一的均匀采样 均匀整数采样隐藏了惊人的复杂度。将随机整数对区间长度取模会引入偏差,因此正确实现必须拒绝生成器的一部分输出。找到精确的拒绝阈值需要一次昂贵的取模运算:当采样器会被复用时,这是合理的初始化;但对于单个值来说太昂贵。Rand 通过 `UniformSampler` trait 暴露了这种区别。构造出的 `UniformInt` 会预先计算阈值并无偏差地采样。与此同时,`Rng::random_range` 走的是单独的 `sample_single` 或 `sample_single_inclusive` 钩子来避免这种初始化,而这些额外的方法很容易被忽略。在 rand 的默认特性下,这个快捷方式使用第二种算法,略微有偏差;可选的 `unbiased` 特性则替换为更复杂的循环版本。我的实现避免了这种分裂,通过惰性计算阈值,让 `UniformInt` 在所有地方都使用一种无偏差的“乘法并拒绝”实现,正如 Daniel Lemire(2018)在 *Fast Random Integer Generation in an Interval*(https://arxiv.org/abs/1805.10941)中描述的那样: ```rust let mut zone = range; loop { let value = rand.next_u64(); // Handle edge case when full range is requested if range == 0 { return value; } let (high, low) = widening_multiply(value, range); if low >= zone { return

相似文章

Correlated RNG

Lobsters Hottest

An article explaining the problem of correlated RNG in procedural generation when multiple systems are seeded with the same initial seed, using examples like Final Fantasy XIV and Slay the Spire, and discussing potential fixes.

Rars:一个主要由LLM编写的Rust RAR实现

Hacker News Top

一个用Rust编写的RAR压缩格式实现,主要由AI语言模型(OpenAI Codex和Claude)编写。如果手动开发可能需要数年时间,但该项目在数周内以低成本完成。

用Rust重写Bun

Hacker News Top

Bun,这个JavaScript运行时和工具链,正在从Zig重写为Rust,以提高内存安全性和稳定性,解决一系列use-after-free和内存泄漏错误。