- Rustの代表的な乱数クレート
randは、日常的な操作が複数のトレイトに分散しているため、より小さな公開・実装サーフェスと一貫した使用体験を備えた urandom を開発した - 高水準の操作を1つの
Random構造体に集約し、Rngトレイトを封印することで、任意の生成器サポートよりも APIの発見しやすさ と内部最適化を優先した - 新しい乱数アルゴリズムは導入せず、Xoshiro256の出力関数を用途別に選択することで、1,000個の
f64生成ベンチマークでrand0.10.2より約 31%高いスループット を記録した - 一様整数サンプリングは、しきい値を遅延計算する単一の 非バイアス実装 により、再利用・単発パスを統合し、
500..20_000範囲のベンチマークでrandの2つのパスより高速だった - 明示的シードの生出力は、対応アーキテクチャとSemVer互換リリースで再現性を保証する一方、任意の生成器接続 と
randの広大な分布・サードパーティ統合エコシステムは手放している
1つに集約した Random API
randの有用な操作は複数のトレイトに分散している- 乱数範囲の生成には
RngExt、シーケンス選択にはIndexedRandom、シャッフルにはSliceRandomが必要 rand0.10は単発呼び出し向けにrand::random_rangeのようなルートレベルのヘルパーを提供している- RNGハンドルを保持したり、選択・シャッフルのようなシーケンス操作を使ったりするには、依然として複数のトレイトのメソッドを探す必要がある
- 乱数範囲の生成には
- preludeでインポートを減らしても、拡張メソッドがRNG、スライス、イテレータのどの型に適用されるかを知る必要があるため、IDEの自動補完だけでは見つけにくい
- urandom は、高水準の利用者向けAPIを1つの
Randomラッパー構造体 に配置するurandom::new()でRandom<urandom::rng::Xoshiro256Rng>を生成するuniform、choose、shuffleを同じオブジェクトから呼び出せる- 自動補完で
random、uniform、chance、choose、shuffle、sampleなどを確認できる - すべて固有メソッドなので、高水準の拡張トレイトを探したりインポートしたりする必要がない
拡張性ではなく最適化を選んだ封印済み Rng
randは低水準RNGトレイトを公開された拡張ポイントとして扱うが、urandomのRngトレイトは封印 されており、対応する生成器をクレート内部で選択して実装する- 任意の生成器を
Randomに接続することはできない - 新しい生成器を追加するには
urandom自体を変更する必要がある
- 任意の生成器を
- よりよいアルゴリズムが目的なら、現在の役割別のデフォルト選択としてXoshiro256とChaChaがすでに定着しており、推奨もゆっくり変わる
- よりよい選択肢が生まれれば、将来のメジャーリリースで採用できる
- 他のプロジェクト・プログラミング言語・レガシーアルゴリズム・特殊ハードウェア・シミュレーション専用生成器と互換にするには、生成器が同じだけでは不十分
- 一様サンプリングやシャッフルなど関連アルゴリズムまで同じである必要があるため、契約全体を実装した専用実装のほうが適している
- 封印済みトレイトにより、未知の生成器や例外状況のための実装契約を設計・文書化せずに、
urandomに必要なプリミティブ操作だけを追加できる- 生成器とアルゴリズムを互いに合わせて特殊化でき、
randでは使えない一部の最適化が可能になる
- 生成器とアルゴリズムを互いに合わせて特殊化でき、
- ほとんどのアプリケーションでは、新しいPRNG実装よりも エントロピーの選択 のほうが有用
- 具体的な生成器はネイティブな
from_seedコンストラクタを公開する ChaCha12Rng::from_seed(seed)のように、明示的シードを使ったRandomを作成できる- 任意のRNG実装は受け付けないが、上級ユーザーが必要とすると予想される拡張ポイントは維持している
- 具体的な生成器はネイティブな
同じアルゴリズムから得た性能向上
urandomは新しい乱数生成アルゴリズムを使っていない- 64ビットシステムで非暗号用途の
urandom::new()とrand::rngs::SmallRngは同じ Xoshiro256系 を使う - 暗号用途の
urandom::csprng()とrand::rngs::StdRngは ChaCha12 を使う - 便利関数
rand::rng()の内部生成器もChaCha12
- 64ビットシステムで非暗号用途の
randの生成器インターフェースは整数ワードとバイト埋めを提供するため、f64が必要な分布でも完全なu64から要求するurandom::Rngはnext_u32、next_u64だけでなくnext_f32とnext_f64も提供する- 浮動小数点乱数は完全なワードより少ない乱数ビットで済む
- 生成器はこれらのメソッドをより安価な出力関数で再定義できる
- Xoshiro実装は状態遷移を共有しつつ、出力パスを分けている
u64にはXoshiro256++を維持するu32と浮動小数点には、上位ビットがその用途向けに設計された、より高速なXoshiro256+を使う
urandom1.0とrand0.10.2でそれぞれ1,000個の乱数を生成したマイクロベンチマーク結果は以下のとおり- Xoshiro
u64: どちらも 814ns - Xoshiro
u32:rand836ns、urandom788ns - Xoshiro
f64:rand1,033ns、urandom788ns - ChaCha12
f64:rand2,199ns、urandom2,011ns
- Xoshiro
- Xoshiro
f64のエンドツーエンドのスループットは約31%高く、実行時間は24%短かったが、同じ処理を行うu64パスは実質的に同等だった - ChaCha12は
next_f64を再定義していないため、性能はおおむね近い - 正確な時間はマシンとコンパイラによって変わり、詳細な条件は 全ベンチマークノート で確認できる
1つに統合した一様サンプリングパス
- 整数を範囲長で単純に剰余演算すると バイアス が生じるため、正しい一様整数サンプリングでは生成器出力の一部を棄却する必要がある
- 正確な棄却しきい値の計算には、コストの高い剰余演算が必要
- サンプラーを繰り返し使うなら、初期設定コストとして受け入れられる
- 値を1つだけ生成する場合、このコストは相対的に大きくなる
randはこの違いをUniformSamplerトレイトとして公開する- 生成された
UniformIntはしきい値を事前計算し、バイアスなしにサンプリングする Rng::random_rangeは初期設定を避けるため、別のsample_singleまたはsample_single_inclusiveフックを使う- デフォルト機能では、単発の短縮パスがややバイアスのある第2のアルゴリズムを使う
- オプションの
unbiased機能は、これをより複雑な反復版に置き換える
- 生成された
urandomはしきい値を 遅延計算 し、再利用範囲と単発範囲の両方に単一の非バイアスな乗算・棄却実装を使う- Daniel Lemireの2018年の論文 Fast Random Integer Generation in an Interval に記載された方式に従う
- ほとんどの実用的な範囲では、最初の候補が除算前に返される
- 最初の候補を返せない場合は、正確なしきい値を計算したうえでバイアスなく反復する
- 全範囲が要求される
range == 0の例外も処理する
- 別メソッドや第2のアルゴリズム、先行設定コスト、バイアスのある高速パスなしに、同じ実装で再利用分布と単発範囲を処理する
500..20_000の範囲で1,000個を抽出したベンチマーク結果は以下のとおり- 再利用
UniformInt:rand1,098ns、urandom950ns - 単発範囲:
rand1,079ns、urandom942ns
- 再利用
randの結果はデフォルト機能基準なので、より速い単発行もややバイアスのあるパスである一方、urandomは非バイアスのまま両方のパスより高速だった
リリースとアーキテクチャをまたぐ再現性
urandomは 再現性 を公開契約の一部として扱う- 同じ明示的シードと同じ低水準RNG呼び出し順序が与えられれば、決定論的生成器の生出力が維持される
- 対応アーキテクチャとSemVer互換リリース全体で安定性を保証する
- 64ビットサーバーと32ビットWebAssemblyクライアントが、再生用に同じ生成器基盤を使用できる
- この互換性を維持するため、32ビットアーキテクチャでは性能を犠牲にしている
randの再現性ポリシー より強い保証であるrandのポータブル生成器とサンプリングアルゴリズムは、マイナーリリースで出力が変わる可能性があるSmallRngとStdRngは明示的にポータブルではなく、プラットフォームやライブラリリリースによっても変わり得る
選択の代償と適用基準
urandomは一般的な操作をRandomに集約し、拡張トレイトなしで見つけやすくしている- 生成器と分布を一緒に設計することで、より安価なXoshiro出力パスと単一の非バイアス一様サンプリングパスを実装している
- 明示的にシード指定された生成器の安定した生ストリームは、決定論的ゲームとシミュレーション に活用できる
- その代わり、任意の生成器を持ち込むことはできず、
randが提供するより大きな分布リストやサードパーティ統合エコシステムも備えていない - 広範なエコシステムが必要なら
randが適しており、小さなAPIサーフェス・発見しやすさ・統合された最適化・強い再現性ポリシーを好むならurandomを選べる - パッケージは crates.io、APIドキュメント、GitHubソース で確認できる
1件のコメント
Lobste.rs のコメント
randをフォークする理由は十分あるが、urandomという名前は/dev/urandomに関連するライブラリのように聞こえる問題意識には同意するが、
pub fn new() -> Random<impl Rng + Clone>は気に入らないアプリケーション全体を
Random<T> where T: Rngとしてパラメータ化すると面倒な作業が増え、コンパイル時間やdyn関連の問題が深刻になる。むしろstruct Randomが具体型を持つようにするか、次善策としてstruct Random<T = rng::Xoshiro256Rng>を選ぶ似たようなもどかしさからすでに自作したことがあるが、フォークではなく、
randより機能はずっと少ない自分と同じ問題を感じた誰かが実際に解決に動いてくれてうれしい。Rust には妙に トレイトスープ ライブラリを作らせる傾向があるように思う
仕事で扱うデータベースの中核データ型は少なくとも 15 個のトレイトを実装しなければならず、自動補完はひどいしドキュメントも混乱している。トレイト数をいくらか減らしたが、循環依存や中核テストを書けなくなる問題によく阻まれる
このライブラリは APOSD の 深いインターフェース と、Filippo のミスしにくいよう設計された暗号関連の仕事の両方を思い出させる。どちらも大いに称賛したい
urandom::new()は 暗号学的に安全な乱数生成器 を返さないので、完全にミス不可能な設計ではない。特に Linux の/dev/urandomは安全なので、なおさら紛らわしい別の
rand代替として、シンプルで高速な乱数生成器fastrandもある。randやurandomより単純だが、機能も少ない