我为什么 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

相似文章

RangeFrom,第 1 部分

Lobsters Hottest

一篇技术博客文章,深入探讨 Rust 的 RangeFrom 类型的历史和背景,追溯其从早期 RFC 到当前及未来范围类型的演变,并计划后续发表关于设计观点的文章。