Why I forked rand
Why I forked rand
为什么我 fork 了 rand
July 27, 2026 — by Casper — rust 2026年7月27日 — 作者:Casper — rust
Rust’s rand crate is the default choice for randomness in the Rust ecosystem. My experience with rand has frustrating papercuts: many everyday operations are hard to discover! That led me to fork and build urandom: a randomness crate with a smaller public and implementation surface, organized around a specific user experience.
Rust 的 rand crate 是 Rust 生态系统中随机数生成的首选。但在使用 rand 的过程中,我遇到了一些令人沮丧的“小麻烦”:许多日常操作很难被发现!这促使我 fork 并构建了 urandom:一个拥有更小公共 API 和实现接口的随机数 crate,它围绕着特定的用户体验进行了组织。
To quote: “Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.” ― Antoine de Saint-Exupéry 引用安托万·德·圣埃克苏佩里的话:“达到完美,不是因为无物可加,而是因为无物可减。”
One place to look
统一的查找入口
With rand, useful operations come from several traits:
在 rand 中,有用的操作分散在多个 trait 中:
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 also provides root-level helpers such as rand::random_range for one-off calls. Once you keep an RNG handle, or need sequence operations such as choosing and shuffling, the method API still comes from several traits. The prelude shortens the imports, but you still need to know to which types the extension methods apply. IDE completion does not help much when the method may belong to the RNG, the slice, the iterator or elsewhere. Discovering the right method often requires already knowing which trait provides it on which type.
Rand 0.10 也提供了一些根级别的辅助函数(如 rand::random_range)用于一次性调用。但一旦你需要持有 RNG 句柄,或者需要进行选择和洗牌等序列操作时,方法 API 依然来自多个不同的 trait。虽然 prelude 缩短了导入路径,但你仍然需要知道这些扩展方法适用于哪些类型。当方法可能属于 RNG、切片、迭代器或其他地方时,IDE 的自动补全帮助有限。发现正确的方法通常需要你预先知道是哪个 trait 在哪种类型上提供了它。
urandom puts its high-level consumer API on one Random wrapper struct:
urandom 将其高级消费 API 统一放在了一个 Random 包装结构体上:
let mut rand = urandom::new();
// rand: urandom::Random<urandom::rng::Xoshiro256Rng>
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);
If you have a Random, editor completion shows you random, uniform, chance, choose, shuffle, sample, and more. These are inherent methods, so there is no high-level extension trait to find or import.
如果你拥有一个 Random 实例,编辑器补全会直接显示 random、uniform、chance、choose、shuffle、sample 等方法。这些都是固有方法(inherent methods),因此无需查找或导入任何高级扩展 trait。
A sealed Rng
封闭的 Rng
rand treats its low-level RNG traits as public extension points. urandom does not: its Rng trait is sealed, and the supported generators are selected and implemented inside the crate. You cannot plug an arbitrary generator into Random. A new generator requires a change to urandom.
rand 将其底层的 RNG trait 视为公共扩展点。而 urandom 则不然:它的 Rng trait 是封闭的(sealed),所支持的生成器均在 crate 内部选择并实现。你无法将任意生成器插入到 Random 中。若要添加新生成器,必须修改 urandom 本身。
I kept coming back to a core question: what material benefit does customizing the generator provide? If the goal is a better algorithm, Xoshiro256 and ChaCha are established default choices for their roles today, and recommendations change slowly. If that changes, urandom can adopt a better choice in a future major release.
我不断回到一个核心问题:自定义生成器到底能带来什么实质性的好处?如果目标是更好的算法,Xoshiro256 和 ChaCha 目前已是各自领域的公认默认选择,且推荐标准更新缓慢。如果未来情况有变,urandom 可以在未来的大版本更新中采用更好的选择。
If the goal is compatibility with another project, programming language, legacy algorithm, specialized hardware, or simulation-specific generator, matching its generator alone is not enough. Uniform sampling, shuffling, and other algorithms must match too. A dedicated implementation of that complete contract is a better fit. 如果目标是为了兼容其他项目、编程语言、遗留算法、专用硬件或特定模拟生成器,仅仅匹配生成器是不够的。均匀采样、洗牌和其他算法也必须匹配。针对该完整契约进行专门实现会更合适。
Sealing Rng lets me shape it around my crate instead. I can add exactly the primitives urandom needs without designing and documenting an implementation contract for unknown generators and their special cases. This lets the generator and algorithms specialize for each other, enabling niche optimizations not available to rand.
封闭 Rng 让我能够围绕我的 crate 来塑造它。我可以精确地添加 urandom 所需的原语,而无需为未知的生成器及其特殊情况设计和记录实现契约。这使得生成器和算法可以相互特化,从而实现 rand 无法做到的细分优化。
For most applications, choosing the entropy is more useful than implementing a new PRNG. The concrete generators expose their native from_seed constructors:
对于大多数应用而言,选择熵源比实现一个新的伪随机数生成器(PRNG)更有用。具体的生成器暴露了它们原生的 from_seed 构造函数:
fn from_my_entropy(seed: [u32; 8]) -> urandom::Random<urandom::rng::ChaCha12Rng> {
urandom::rng::ChaCha12Rng::from_seed(seed)
}
This is narrower than accepting any RNG implementation, but it preserves the extension point I expect advanced users to need. 这比接受任何 RNG 实现的范围更窄,但它保留了我预期高级用户可能需要的扩展点。
Beating rand without a better algorithm
无需更优算法即可超越 rand
My fork is not based on novel random-number algorithms. On 64-bit systems, urandom::new() and rand::rngs::SmallRng use the same Xoshiro256 family for non-cryptographic use. urandom::csprng() and rand::rngs::StdRng use ChaCha12 for cryptographic use. Rand’s convenient rand::rng() handle is backed by ChaCha12.
我的 fork 并非基于什么新颖的随机数算法。在 64 位系统上,urandom::new() 和 rand::rngs::SmallRng 在非加密用途上使用相同的 Xoshiro256 系列。urandom::csprng() 和 rand::rngs::StdRng 在加密用途上使用 ChaCha12。Rand 便捷的 rand::rng() 句柄底层也是由 ChaCha12 支持的。
rand’s generator interface exposes integer words and byte filling. A distribution that wants an f64 starts by asking for a full u64. urandom::Rng also has specialized next_f32 and next_f64:
rand 的生成器接口暴露了整数位(integer words)和字节填充。一个需要 f64 的分布通常会先请求一个完整的 u64。urandom::Rng 则拥有专门的 next_f32 和 next_f64:
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
}
Random floats need fewer random bits than a full word. A generator can therefore override these methods with a cheaper output function. The Xoshiro family of algorithms support exactly that. The implementation retains Xoshiro256++ for u64, while u32 and floats use the faster Xoshiro256+. Both share the same state advance, but Xoshiro256+ has a cheaper output function whose upper bits are designed for this use case.
随机浮点数所需的随机位数比一个完整的字(word)要少。因此,生成器可以用更廉价的输出函数来重写这些方法。Xoshiro 系列算法恰好支持这一点。该实现为 u64 保留了 Xoshiro256++,而 u32 和浮点数则使用更快的 Xoshiro256+。两者共享相同的状态推进机制,但 Xoshiro256+ 拥有更廉价的输出函数,其高位正是为此用例设计的。
On my system, comparing the current urandom 1.0 implementation with rand 0.10.2, the end-to-end Xoshiro f64 benchmark measured about 31% higher throughput (24% faster) in this microbenchmark. The u64 paths, where both do the same work, were effectively tied.
在我的系统上,对比当前的 urandom 1.0 实现与 rand 0.10.2,在这次微基准测试中,端到端的 Xoshiro f64 吞吐量提高了约 31%(快了 24%)。而在两者工作量相同的 u64 路径上,性能基本持平。
| 1,000 random values | 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 |
Exact timings depend on the machine and compiler. Here, the richer interface lets Xoshiro select a cheaper output function for u32 and floats. ChaCha12 does not override next_f64; hence the equivalent performance.
具体的耗时取决于机器和编译器。在这里,更丰富的接口让 Xoshiro 能够为 u32 和浮点数选择更廉价的输出函数。ChaCha12 没有重写 next_f64,因此性能相当。
Unified uniform sampling
统一的均匀采样
Uniform integer sampling hides a surprising amount of complexity. Reducing a random integer modulo the length of a range introduces bias, so a correct implementation must reject part of the generator’s output. Finding the exact rejection threshold requires an expensive modulo operation: reasonable setup when a sampler will be reused, but expensive for a single value. Rand exposes that distinction through its UniformSampler trait. A constructed UniformInt calculates the threshold up front and samples without bias. Rng::random_range, meanwhile, goes through the separate sample_sin…
均匀整数采样隐藏了惊人的复杂性。将随机整数对范围长度取模会引入偏差,因此正确的实现必须拒绝部分生成器的输出。寻找精确的拒绝阈值需要昂贵的取模运算:当采样器被重复使用时,这种设置是合理的,但对于单个值来说则过于昂贵。Rand 通过其 UniformSampler trait 暴露了这种区别。构造出的 UniformInt 会预先计算阈值并进行无偏采样。与此同时,Rng::random_range 则通过单独的 sample_sin…(注:原文此处截断)