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型錯誤。