我为什么 fork 了 rand
摘要
作者解释了为什么将 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
相似文章
关于 /dev/urandom 的常见误解 (2014)
澄清了关于 /dev/urandom 和 /dev/random 的常见误解,说明 /dev/urandom 是类 Unix 系统中加密随机性的首选来源。
RangeFrom,第 1 部分
一篇技术博客文章,深入探讨 Rust 的 RangeFrom 类型的历史和背景,追溯其从早期 RFC 到当前及未来范围类型的演变,并计划后续发表关于设计观点的文章。
为何常见的Rust包依赖C代码?(2023)
本文探讨为何许多常见的Rust包依赖于C代码,可能讨论技术原因及其对生态系统的影响。
RangeFrom,第二部分:我对设计缺陷的看法
本文批判了 Rust 的 RangeFrom 迭代器设计,突出了溢出处理和意外行为可能导致无限循环或崩溃的问题。
我们如何(及为何)将生产环境的C++前端基础设施重写为Rust
NearlyFreeSpeech.NET 将其生产环境的C++前端基础设施(nfsncore)重写为Rust,该系统负责所有传入请求的路由、缓存和访问控制。迁移的动机是Rust的安全性保证、性能、生态系统优势以及老化的C++代码库的局限性。