tinyrand
轻巧的RNG规范和Rust中的几种超快实现。 tinyrand
是no_std
,不使用堆分配器。
tinyrand
?std
,这意味着它可以嵌入 - 它在微控制器和裸机(无OS)环境上运行。Mock
测试代码,该代码取决于随机数。也就是说,如果您关心代码覆盖范围。以下是几个值得注意的PRNG的比较。
prng | 算法 | 带宽(GB/S) | |
---|---|---|---|
rand | chacha12 | 2.4 | |
tinyrand | SplitMix | 6.5 | |
tinyrand | Xorshift | 6.7 | |
fastrand | Wyrand | 7.5 | |
tinyrand | Wyrand | 14.6 |
TL; DR: tinyrand
比fastrand
快2倍,比rand
快6倍。
无法确定某个PRNG是否良好。答案是概率的。所有三种算法都在测试的顽固分子弹幕上表现得很好,但是Wyrand和SplitMix比Xorshift好一些。 (在308亿样品中进行了测试。)这意味着tinyrand
产生的数字看起来足够随机,并且可能适合在大多数应用中使用。
tinyrand
算法在密码上不是密码的,这意味着可以通过观察数字序列来猜测下一个随机数。 (就此而言,或者上一个数字。)如果您需要强大的csprng,则强烈建议您与rand
一起使用。 csprng通常要慢得多,大多数人都不需要一个。
cargo add tinyrand
生成数字需要一个Rand
实例。在这里,我们使用StdRand
,这是默认/推荐RNG的别名。 (目前设置为Wyrand
,但将来可能会发生变化。)
use tinyrand :: { Rand , StdRand } ;
let mut rand = StdRand :: default ( ) ;
for _ in 0 .. 10 {
let num = rand . next_u64 ( ) ;
println ! ( "generated {num}" ) ;
}
同样,我们可以生成其他类型的数字:
use tinyrand :: { Rand , StdRand } ;
let mut rand = StdRand :: default ( ) ;
let num = rand . next_u128 ( ) ;
println ! ( "generated wider {num}" ) ;
next_uXX
方法在指定类型的整个未签名范围内生成数字。通常,我们希望在特定范围内有一个数字:
use tinyrand :: { Rand , StdRand , RandRange } ;
let mut rand = StdRand :: default ( ) ;
let tasks = vec ! [ "went to market" , "stayed home" , "had roast beef" , "had none" ] ;
let random_index = rand . next_range ( 0 ..tasks . len ( ) ) ;
let random_task = tasks [ random_index ] ;
println ! ( "This little piggy {random_task}" ) ;
另一个常见的用例是生成bool
s。我们可能还想为二进制结果分配权重:
use tinyrand :: { Rand , StdRand , Probability } ;
let mut rand = StdRand :: default ( ) ;
let p = Probability :: new ( 0.55 ) ; // a slightly weighted coin
for _ in 0 .. 10 {
if rand . next_bool ( p ) {
// expect to see more heads in the (sufficiently) long run
println ! ( "heads" ) ;
} else {
println ! ( "tails" ) ;
}
}
有时我们需要线程睡一会儿,等待病情。当许多线程入睡时,通常建议它们随机退缩以避免踩踏。
use tinyrand :: { Rand , StdRand , RandRange } ;
use core :: time :: Duration ;
use std :: thread ;
use tinyrand_examples :: SomeSpecialCondition ;
let mut rand = StdRand :: default ( ) ;
let condition = SomeSpecialCondition :: default ( ) ;
let base_sleep_micros = 10 ;
let mut waits = 0 ;
while !condition . has_happened ( ) {
let min_wait = Duration :: ZERO ;
let max_wait = Duration :: from_micros ( base_sleep_micros * 2u64 . pow ( waits ) ) ;
let random_duration = rand . next_range ( min_wait..max_wait ) ;
println ! ( "backing off for {random_duration:?}" ) ;
thread :: sleep ( random_duration ) ;
waits += 1 ;
}
在Rand
上调用Default::default()
以恒定的种子初始化。这对于可重复性非常有用,但是导致相同的“随机”数字运行,这不是大多数人所需要的。
tinyrand
是一个no_std
板条箱,可悲的是,当人们无法对基础平台做出假设时,就没有一个好的便携式方法来生成熵。在大多数应用中,一个可能的时钟,但是像SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
一样琐碎的东西可能不会总是可用的。
如果您可以使用熵源,则可以这样Rrnd
:
use tinyrand :: { Rand , StdRand , Seeded } ;
let seed = tinyrand_examples :: get_seed_from_somewhere ( ) ; // some source of entropy
let mut rand = StdRand :: seed ( seed ) ;
let num = rand . next_u64 ( ) ;
println ! ( "generated {num}" ) ;
您还可以考虑使用getrandom
,这是用于检索熵数据的跨平台方法。
如果一个人不在乎no_std
,则不应受其局限性的约束。要从系统时钟播种,您可以选择进入std
:
cargo add tinyrand-std
现在,我们有一个可以使用的ClockSeed
,这也实现了Rand
特征。 ClockSeed
通过将纳秒时间戳的上部64位(来自SystemTime
)带有下部64位,从而衍生了u64
。它不适合加密使用,但对于大多数通用应用来说就足够了。
use tinyrand :: { Rand , StdRand , Seeded } ;
use tinyrand_std :: clock_seed :: ClockSeed ;
let seed = ClockSeed :: default ( ) . next_u64 ( ) ;
println ! ( "seeding with {seed}" ) ;
let mut rand = StdRand :: seed ( seed ) ;
let num = rand . next_u64 ( ) ;
println ! ( "generated {num}" ) ;
tinyrand-std
板条箱还包括一个种子的螺纹 - 本地Rand
实现:
use tinyrand :: Rand ;
use tinyrand_std :: thread_rand ;
let mut rand = thread_rand ( ) ;
let num = rand . next_u64 ( ) ;
println ! ( "generated {num}" ) ;
良好的测试覆盖范围有时很难实现;因此,当应用取决于随机性或其他非确定性来源时,双重偶然。 tinyrand
带有模拟RNG,可对您的代码执行进行细粒度的控制。
该模拟使用alloc
板条箱,因为它需要闭合分配。因此,模拟被分配为选择包装:
cargo add tinyrand-alloc
在基层,模拟Mock
配置为少数代表。委托是一个封闭式,当系统测试的系统调用特定性状方法时,该委托人被模拟引用。该模拟还保持了内部调用状态,该状态可以跟踪行使特定代表的次数。因此,您不仅可以嘲笑Rand
特征的行为,而且还可以验证一个类型的类型数量。
代表是由测试案例指定的,而模拟实例则将其传递给正在测试的系统作为Rand
实现。目前,支持三种代表类型:
FnMut(&State) -> u128
- 当next_uXX()
方法之一在模拟上调用时,被调用。 ( uXX
是u16
, u32
, u64
, u128
或usize
之一。)代表返回下一个“随机”号码,最高可达128位。该宽度旨在容纳u128
,这是Rand
支持的最大类型。如果请求较窄的类型之一,则模拟只需返回较低的位即可。 (例如,对于u32
,模拟的值使用引擎盖下的as u32
截断。)FnMut(Surrogate, Probability) -> bool
- 调用next_bool(Probability)
方法时调用。FnMut(Surrogate, u128) -> u128
- next_lim
或next_range
被调用时。从绝对基础开始,让我们模拟next_uXX()
返回常数。然后,我们将检查我们的模拟电话多少次。
use tinyrand :: Rand ;
use tinyrand_alloc :: Mock ;
let mut rand = Mock :: default ( ) . with_next_u128 ( |_| 42 ) ;
for _ in 0 .. 10 {
assert_eq ! ( 42 , rand.next_usize ( ) ) ; // always 42
}
assert_eq ! ( 10 , rand.state ( ) .next_u128_invocations ( ) ) ;
尽管很简单,但这种情况实际上很普遍。 fixed(uXX)
函数也可以实现相同的功能。
use tinyrand :: Rand ;
use tinyrand_alloc :: { Mock , fixed } ;
let mut rand = Mock :: default ( ) . with_next_u128 ( fixed ( 42 ) ) ;
assert_eq ! ( 42 , rand.next_usize ( ) ) ; // always 42
由于委托是常规关闭,因此我们可以与封闭范围中的变量结合。这使我们几乎无限地控制了模拟的行为。
use tinyrand :: Rand ;
use tinyrand_alloc :: Mock ;
use core :: cell :: RefCell ;
let val = RefCell :: new ( 3 ) ;
let mut rand = Mock :: default ( ) . with_next_u128 ( |_| * val . borrow ( ) ) ;
assert_eq ! ( 3 , rand.next_usize ( ) ) ;
// ... later ...
* val . borrow_mut ( ) = 17 ;
assert_eq ! ( 17 , rand.next_usize ( ) ) ;
即使创建和行使模拟后,也可以在任何时候重新分配代表:
use tinyrand :: Rand ;
use tinyrand_alloc :: { Mock , fixed } ;
let mut rand = Mock :: default ( ) . with_next_u128 ( fixed ( 42 ) ) ;
assert_eq ! ( 42 , rand.next_usize ( ) ) ;
rand = rand . with_next_u128 ( fixed ( 88 ) ) ; // the mock's behaviour is now altered
assert_eq ! ( 88 , rand.next_usize ( ) ) ;
next_u128
代表的签名采用State
参考,该参考捕获了调用模拟的次数。 (仅在调用完成后才增加计数。)让我们写一个模拟,返回从调用状态得出的“随机”号码。
use tinyrand :: Rand ;
use tinyrand_alloc :: Mock ;
let mut rand = Mock :: default ( ) . with_next_u128 ( |state| {
// return number of completed invocations
state . next_u128_invocations ( ) as u128
} ) ;
assert_eq ! ( 0 , rand.next_usize ( ) ) ;
assert_eq ! ( 1 , rand.next_usize ( ) ) ;
assert_eq ! ( 2 , rand.next_usize ( ) ) ;
当我们期望将模拟调用几次并且每个调用都应返回不同结果时,这很有用。 counter(Range)
函数可以实现类似的结果,该功能循环通过指定的数字范围,方便地在边界上包裹:
use tinyrand :: Rand ;
use tinyrand_alloc :: { Mock , counter } ;
let mut rand = Mock :: default ( ) . with_next_u128 ( counter ( 5 .. 8 ) ) ;
assert_eq ! ( 5 , rand.next_usize ( ) ) ;
assert_eq ! ( 6 , rand.next_usize ( ) ) ;
assert_eq ! ( 7 , rand.next_usize ( ) ) ;
assert_eq ! ( 5 , rand.next_usize ( ) ) ; // start again
通过仅提供next_u128
代表,我们可以影响Rand
特征中所有其他方法的结果,因为它们都源自相同的随机来源,并最终将我们的代表称为“引擎盖”……从理论上讲!实际上,事情要复杂得多。
派生的Rand
方法,例如next_bool(Probability)
, next_lim(uXX)
和next_range(Range)
都以不同的概率分布来支持。例如, next_bool
从Bernoulli发行版中汲取,而next_lim
和next_range
则使用带有添加的偏差层的缩放统一分布。此外,各种分布之间的映射是一个内部实现细节,可能会发生变化。单独使用辩护层具有多种实现,可针对不同宽度的类型进行了优化。换句话说,从next_u128
到next_bool
, next_lim
和next_range
and notivial映射;没有计算器和一些模块化算术知识,这不是您要嘲笑的。
幸运的是, Rand
让我们“绕过”这些映射功能。这是其他两个代表进来的地方。在下面的示例中,我们嘲笑next_bool
的结果。
use tinyrand :: { Rand , Probability } ;
use tinyrand_alloc :: Mock ;
let mut rand = Mock :: default ( ) . with_next_bool ( |_ , _| false ) ;
if rand . next_bool ( Probability :: new ( 0.999999 ) ) {
println ! ( "very likely" ) ;
} else {
// we can cover this branch thanks to the magic of mocking
println ! ( "very unlikely" ) ;
}
next_bool
代表将递给Surrogate
结构,这既是Rand
实现,又是调用状态的守护者。代理使我们能够得出bool
,因此:
use tinyrand :: { Rand , Probability } ;
use tinyrand_alloc :: Mock ;
let mut rand = Mock :: default ( ) . with_next_bool ( |surrogate , _| {
surrogate . state ( ) . next_bool_invocations ( ) % 2 == 0
} ) ;
assert_eq ! ( true , rand.next_bool ( Probability ::new ( 0.5 ) ) ) ;
assert_eq ! ( false , rand.next_bool ( Probability ::new ( 0.5 ) ) ) ;
assert_eq ! ( true , rand.next_bool ( Probability ::new ( 0.5 ) ) ) ;
assert_eq ! ( false , rand.next_bool ( Probability ::new ( 0.5 ) ) ) ;
代理人还可以让代表从模拟内部调用模拟方法。
最后的代表由于其同构依据而用于模拟next_lim
和next_range
方法。在引擎盖下, next_range
委托next_lim
,因此,对于任何一对限制边界( M
, N
), M
< N
, next_range(M..N)
= M
+ next_lim(N - M)
。这就是在实践中都被嘲笑的方式:
use tinyrand :: { Rand , RandRange } ;
use tinyrand_alloc :: Mock ;
enum Day {
Mon , Tue , Wed , Thu , Fri , Sat , Sun
}
const DAYS : [ Day ; 7 ] = [ Day :: Mon , Day :: Tue , Day :: Wed , Day :: Thu , Day :: Fri , Day :: Sat , Day :: Sun ] ;
let mut rand = Mock :: default ( ) . with_next_lim_u128 ( |_ , _| 6 ) ;
let day = & DAYS [ rand . next_range ( 0 .. DAYS . len ( ) ) ] ;
assert ! ( matches! ( day, Day :: Sun ) ) ; // always a Sunday
assert ! ( matches! ( day, Day :: Sun ) ) ; // yes!!!
tinyrand
如何测试?本节简要介绍了tinyrand
测试方法。它是针对那些 -
tinyrand
测试过程分为四个层:
tinyrand
的基本理智。换句话说,每行代码至少一次行使一次,基本期望得到维持,并且可能没有微不足道的缺陷。tinyrand
的PRNG的特定特性。这些是正式的假设检验,假设源是随机的(零假设),并寻找证据来消除这种假设(替代假设)。该单元测试不是旨在主张数值质量;它们本质上纯粹是功能性的。目标包括 -
tinyrand
建立在以下理念的基础上,即如果未经证实行使一系列代码,则应将其删除。该规则没有例外。bool
s的产生中, true
结果与false
加权。从均匀分布到自定义的映射功能是不平凡的,需要一个偏见层。 tinyrand
根据单词宽度使用不同的偏见方法。域转换测试的目的是验证该功能是否正常工作,并且正在进行拒绝采样。但是,它没有验证借记下的数值属性。 合成基准用于锻炼tinyrand
prngs的热路径,将结果与同伴文库进行比较。基准测试以各种单词长度,转换/偏见和加权bool
的产生来测试数字的产生。 CI测试中还包括这些基准的子集,使得比较跨提交版本的tinyrand
的性能变得更加容易。
tinyrand
捆绑了一个集成的统计测试套件,灵感来自Diehard,Dieharder和Nist SP 800-22。 tinyrand
套件的肯定比这些测试中的任何一个都要小得多。目的不是要在该领域复制已经实质性且易于访问的工作,而是要创建一个安全网,该安全网既可以在检测常见异常方面都非常有效,又可以在每个提交中都足够快地运行。
包括以下测试。
Rand
实例进行了一系列Bernoulli试验,验证位设置为1的次数在预期范围内。对于每个随后的试验,面罩被一个向左移动,并重新测试了假设。测试进行了几个周期。每个周期包括64次Bernoulli试验( u64
的每一点)。bool
。该测试包括一系列在每个试验中具有不同(随机选择的)权重的伯努利试验,模拟了硬币翻转。在每个试验中,H0断言源是随机的。 (即,“头”的数量属于统计上可接受的间隔。)u64
段之间交替进行的。在每个试验中,我们都假设单个位的值是IID的概率为0.5,可以验证将位设置为1的次数在预期范围内。对于随机来源,1(和0)的数量遵循Bernoulli过程。 tinyrand
的每个测试都不仅针对自己的PRNG进行,而且针对故意有缺陷的实现,这些实现用于验证测试的功效。这些测试必须始终无法拒绝正确的PRNG的H0,并接受H1的H1。
统计检验本身是从随机值中播种的。随机性用于播种正在测试的PRNG(每个试验都是独立种子),将权重分配给Bernoulli实验,选择用于测试转换功能的整数范围和检测偏差,控制碰撞的控制值等等。我们将rand
软件包用作控制PRNG,以便tinyrand
中的缺陷不能无意中以掩盖自身的方式颠覆测试。测试测试,因此,尽管它们似乎是通过参数空间进行的随机偏移,但它们的参数选择是完全确定性的,因此可以重复。这是必不可少的,这是由于I型错误的可能性(错误地拒绝零假设),这不得间断地发生,尤其是在CI环境中。换句话说,随机性的测试不能偶然。
测试随机性假设的一种方法是选择一组参数(例如,整数生成范围M
.. N
或从Bernoulli分布获得true
概率)并进行长期运行,在大型随机样本中寻求异常。理由是样品越大,其可能包含可检测到的异常的可能性越大。通常,对于发现仅在非常具体的条件下可能影响PRNG的某些异常情况,这通常不是很有效。例如,书写较差的偏见功能对于大多数小整数范围甚至一些大型范围(接近两个力量的范围)仍然可以表现良好。如果测试选择参数不利,那么无论测试这些参数多么详尽,它都可能找不到异常。
测试PRNG的一种更好的方法是将多样性引入测试制度 - 进行大量具有不同参数的小型试验,而不是一个非常大的试验。这正是tinyrand
统计检验所做的事情 - 进行多个试验,并随机(但决定性地)选定的参数。这立即暴露了多重比较问题。考虑一个先验理想的prng。它经常会根据某些商定的措施产生“随机”的数字。但是有时,它会产生以同一度量出现非随机的输出。例如,即使是理想的来源也会产生很长的一个或零。实际上,如果不这样做也会使其变得非随机。不幸的是,这将产生一个P值,即使在某个时候也将失败,即使是最轻松的测试也将失败。这是单个假设检验的问题,但是在多个假设检验中会按比例加剧。
tinyrand
内置测试使用Holm-Bonferroni顺序校正方法解决了此问题。 Holm-Bonferroni校正会抑制I型错误,同时保持良好的统计能力 - 对II型错误的抑制。对于tinyrand
的需求,它似乎表现良好,尤其是看到数量试验通常保留在100至1000范围内。 ( tinyrand
测试的设计非常快,这对试验的数量进行了实际限制 - 理想情况下,所有统计测试均应在几秒钟内完成,以作为常规开发流程的一部分。)
Dieharder Test Suite扩展了Marsaglia的原始顽固测试。它与大量测试捆绑在一起,需要很长时间(〜1小时)才能完成。 tinyrand
具有将随机输出泵送到Dieharder的实用程序,该输出通常是在临时运行的。当PRNG经历材料变化时,应运行顽固的电池,这是很少的 - 一旦实现了PRNG算法,除非被重构或发现某些缺陷,否则通常会保持不变。 DieHarder可以说,对于用tinyrand
构建和测试实验性PRNG更有用。其他三层测试足以维持tinyrand
软件包。
对抗顽固的tinyrand
:
cargo run --release --bin random -- wyrand 42 binary 1T | dieharder -g 200 -a
上面的命令使用wyrand prng,并用数字42播种,生成超过1万亿位64位单词的二进制输出。它的stdout
被抽到dieharder
。 (实际上,Dieharder的数量将低于310亿。)
一个谨慎的词:顽固的人没有在多个假设测试中处理I型错误的机制 - 部分是因为测试的类型不同,而不仅仅是参数。 Dieharder将假设检验限制为单个测试的范围;没有总体假设将PRNG根据经过的测试的数量分类为适合或不适合,或者以其他方式调整置信度水平以说明I型错误。