.NET 6.0 以降のRandom
クラスは、シード指定時と未指定時で使用されるアルゴリズムが異なり、未指定時のみ xoshiro 法が用いられる。
参考:.NET 6 (Preview) における System.Random の実装変更 - 屋根裏工房改
シード指定では従来型のアルゴリズムが用いられるが、こちらはあまり良くないとされている。
参考:.NET System.Random の実装と欠陥について ~ 重箱の隅をつつきたおす ~ - 屋根裏工房改
品質もパフォーマンスも xoshiro 法が優れるため常にそちらを使いたいが、通常はシードを与えて使うことができない。
単純に優れている方があるのに(おそらく互換性のために)使えない…… なんとかならないか?
まず前提以前の話として、別の乱数生成器を使うという選択肢がある。
例えば Unity の乱数は Xorshift 法で、シード(というか、状態)を設定可能。
前提を標準ライブラリに置く。
普通なら内部状態の取得にはリフレクション。
リフレクションで値を取得しておいて、それで状態を復元することで擬似的にシード付き初期化になる。
Random rand = new(); var xoshiro = typeof(Random)? .GetField("_impl", BindingFlags.NonPublic | BindingFlags.Instance)? .GetValue(rand); var fields = Type.GetType("System.Random+XoshiroImpl, System.Private.CoreLib")? .GetFields(BindingFlags.NonPublic | BindingFlags.Instance)!; var s0Info = fields.First(v => v.Name == "_s0"); var s1Info = fields.First(v => v.Name == "_s1"); var s2Info = fields.First(v => v.Name == "_s2"); var s3Info = fields.First(v => v.Name == "_s3"); // 内部状態を保存 var state = ((ulong)s0Info.GetValue(xoshiro)!, (ulong)s1Info.GetValue(xoshiro)!, (ulong)s2Info.GetValue(xoshiro)!, (ulong)s3Info.GetValue(xoshiro)!); // int を16回ほど出力 Span<int> nums1 = stackalloc int[16]; for (int i = 0; i < nums1.Length; i++) nums1[i] = rand.Next(256); Console.WriteLine(nums1.ToListString()); // ToListString という拡張メソッドがあるものとする // 復元 var (s0, s1, s2, s3) = state; s0Info.SetValue(xoshiro, s0); s1Info.SetValue(xoshiro, s1); s2Info.SetValue(xoshiro, s2); s3Info.SetValue(xoshiro, s3); // もう一度 int を出力して確認 Span<int> nums2 = stackalloc int[16]; for (int i = 0; i < nums2.Length; i++) nums2[i] = rand.Next(256); Console.WriteLine($"SequenceEqual: {nums1.SequenceEqual(nums2)}"); Console.WriteLine(nums2.ToListString()); // [140, 127, 218, 221, 180, 182, 73, 117, 228, 208, 43, 37, 209, 157, 142, 244] // SequenceEqual: True // [140, 127, 218, 221, 180, 182, 73, 117, 228, 208, 43, 37, 209, 157, 142, 244]
再現を無事確認。
さてここでリフレクション以外の手段として、以前学んだ書込み用ビューを用いる手法を実験する。
参考:【C#】Span を List<T>.AddRange する - てくメモ
次のようなビューを用意する。
// ✕適用不可: Random.Shared (中身は Random の継承クラス ThreadSafeRandom) // ✕適用不可: シードを与えて new した Random // ✕適用不可: .NET6.0 より前 internal sealed class RandomXoshiroView { public XoshiroView _impl; internal sealed class XoshiroView { public ulong _s0, _s1, _s2, _s3; } }
コメントで色々注記を添えたけれど、とりあえず脇に置いて進める。
new Random()
したインスタンスをこのビューにUnsafe.As
することで状態の読み書きを行うことができる。
状態を取るState
メソッドと、設定するInitState
メソッドを以下のとおり作成。
public static (ulong s0, ulong s1, ulong s2, ulong s3) State(this Random rand) { var view = Unsafe.As<Random, RandomXoshiroView>(ref rand)._impl; return (view._s0, view._s1, view._s2, view._s3); } public static void InitState(this Random rand, in (ulong s0, ulong s1, ulong s2, ulong s3) state) { var view = Unsafe.As<Random, RandomXoshiroView>(ref rand)._impl; view._s0 = state.s0; view._s1 = state.s1; view._s2 = state.s2; view._s3 = state.s3; }
先ほどリフレクションで取得した状態を叩き込んで、同じ出力になることを確認してみる。
// 先ほど取得した state を設定 rand.InitState(state); // 確認 Span<int> nums3 = stackalloc int[16]; for (int i = 0; i < nums3.Length; i++) nums3[i] = rand.Next(256); Console.WriteLine($"SequenceEqual: {nums1.SequenceEqual(nums3)}"); Console.WriteLine(nums3.ToListString()); // SequenceEqual: True // [140, 127, 218, 221, 180, 182, 73, 117, 228, 208, 43, 37, 209, 157, 142, 244]
OK!
また、状態を取得・設定できるということは、勝手に遷移させられる。
xoshiro 法のRandom
はサンプリングでulong
を得るが、それを取得する手段がない。
ということで、それをエミュレートしてみる。
参考:Source Browser
public static ulong NextUInt64(this Random rand) { var view = Unsafe.As<Random, RandomXoshiroView>(ref rand)._impl; ulong s0 = view._s0, s1 = view._s1, s2 = view._s2, s3 = view._s3; ulong result = BitOperations.RotateLeft(s1 * 5, 7) * 9; ulong t = s1 << 17; s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3; s2 ^= t; s3 = BitOperations.RotateLeft(s3, 45); view._s0 = s0; view._s1 = s1; view._s2 = s2; view._s3 = s3; return result; }
状態が正しく遷移するか確認する。
NextDouble()
は内部的に一回だけ状態を消費するので、それと較べる。
Random rand = new(); var state0 = rand.State(); // 状態を保存 rand.NextDouble(); var state1 = rand.State(); // NextDouble 後の状態 rand.InitState(state0); // 復元 rand.NextUInt64(); var state2 = rand.State(); // 上記拡張メソッドの NextUInt64 後の状態 // 正しく遷移したら true のはず Console.WriteLine($"state1 == state2: {state1 == state2}"); // state1 == state2: True
OK!
―― と、色々書いてきたけれど、この方法はやはり安全さが……
環境(バージョン)次第でダメ、Random.Shared
はダメ、シード付きで初期化してるとダメ。
注意すればいいのだけれど、注意が必要という時点でまさに Unsafe。
じゃあなぜその方法で記事を?
ビューをUnsafe.As
するという銃を握ったら…… 撃ちたくなって…… 😥
品質の良い乱数をシード付きで使いたいときは、素直に別の乱数生成器を素直に使うのが安全。