15

I would like to be able to get the actual state or seed or whatever of System.Random so I can close an app and when the user restarts it, it just "reseeds" it with the stored one and continues like it was never closed.

Is it possible?

Using Jon's idea I came up with this to test it;

static void Main(string[] args)
{
    var obj = new Random();
    IFormatter formatter = new BinaryFormatter();
    Stream stream = new FileStream("c:\\test.txt", FileMode.Create, FileAccess.Write, FileShare.None);
    formatter.Serialize(stream, obj);
    stream.Close();
    for (var i = 0; i < 10; i++)
        Console.WriteLine(obj.Next().ToString());

    Console.WriteLine();

    formatter = new BinaryFormatter();
    stream = new FileStream("c:\\test.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
    obj = (Random)formatter.Deserialize(stream);
    stream.Close();
    for (var i = 0; i < 10; i++)
        Console.WriteLine(obj.Next().ToString());

    Console.Read();
}
Peter O.
  • 32,158
  • 14
  • 82
  • 96
Fredou
  • 19,848
  • 10
  • 58
  • 113
  • I've created a new lib to deal with this issue: https://github.com/RafalSzefler/NPrng It offers few different prng algorithms with serializers. – freakish Oct 19 '21 at 07:51

5 Answers5

15

It's serializable, so you may find you can just use BinaryFormatter and save the byte array...

Sample code:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class Program
{
    public static void Main(String[] args)
    {
        Random rng = new Random();
        Console.WriteLine("Values before saving...");
        ShowValues(rng);

        BinaryFormatter formatter = new BinaryFormatter(); 
        MemoryStream stream = new MemoryStream();
        formatter.Serialize(stream, rng);

        Console.WriteLine("Values after saving...");
        ShowValues(rng);

        stream.Position = 0; // Rewind ready for reading
        Random restored = (Random) formatter.Deserialize(stream);

        Console.WriteLine("Values after restoring...");
        ShowValues(restored);       
    }

    static void ShowValues(Random rng)
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(rng.Next(100));
        }
    }
}

Results on a sample run are promising:

Values before saving...
25
73
58
6
33
Values after saving...
71
7
87
3
77
Values after restoring...
71
7
87
3
77

Admittedly I'm not keen on the built-in serialization, but if this is for a reasonably quick and dirty hack, it should be okay...

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • I will try on my side, i will keep an eye here at the same time – Fredou Nov 18 '11 at 20:49
  • thanks :-), working nicely, I didn't think of serialization :-( – Fredou Nov 18 '11 at 20:56
  • by looking at the file generated, it seem it storing all or some of the random number generated previously. This could get big. you do know how to fix this issue? – Fredou Nov 18 '11 at 21:00
  • @Fredou most likely it is using the numbers it generates as more internal state, Once you have enough data it should start dropping old values. I do not know how much this state will grow to but you can experiment and find it out by doing a for loop, saving after X number of iterations to a new file name and see when the file size stops growing. If you do this report back, I am interested in knowing too! – Scott Chamberlain Nov 18 '11 at 21:13
  • @ScottChamberlain, it seem to stay at 317 bytes in my small testing – Fredou Nov 18 '11 at 21:19
  • @Fredou how many numbers generated does it take for it to get to that size? – Scott Chamberlain Nov 18 '11 at 21:26
  • 2
    I'd expect it to be that size the whole time - it's just the state of the object, with overhead for remembering the class name, assembly name etc. – Jon Skeet Nov 18 '11 at 21:28
  • @ScottChamberlain, what is weird is, even with no generation or 1000 generation, it's stay at 317 bytes – Fredou Nov 18 '11 at 21:31
  • 5
    @Fredou: It's not weird at all - that's just a snapshot of the state of the random number generator. It would be weird if the size changed! – Jon Skeet Nov 18 '11 at 21:32
  • Is this possible in a .NET Standard 2.1 library? I just tried it and ran into `Type 'System.Random' in Assembly 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' is not marked as serializable.` – Simon K. Nov 12 '20 at 21:05
  • I've looked into Random object in a debugger. You can easily extract that int[56] SeedArray with reflection and restore back the same way. But I'd like to be able to extract single int though. That would be much nicer to reseed it back in the conventional way. But looking on https://referencesource.microsoft.com/#mscorlib/system/random.cs,3f293365c6d65ffd There seems like SeedArray[55] + 161803398 gets initial seed. Array elements gets changed avery time random number gets generated. – zORg Alex May 19 '21 at 09:15
  • @zORgAlex: "You can easily extract that int[56] SeedArray with reflection and restore back the same way" - if you're content for your code to be broken in the next release, yes. I would strongly advise against doing that. (Whereas I'd expect the library maintainers to ensure that the binary format stays compatible.) – Jon Skeet May 19 '21 at 09:18
  • And I've forgot to mention that you'd also want to grab inext and inextp values to that array to save and restore. I've spent 2h to figure that Random SeedArray isn't the only factor. – zORg Alex May 19 '21 at 12:51
  • 317 bytes for a random number generator is quite a lot to be honest. The `Random` class is quite heavy, I suppose because it is a subtractive generator (AFAIK). Plus I don't really like hacking it with reflection. So I've created a library that deals with it: https://github.com/RafalSzefler/NPrng All of the generators are as good as `Random` (and many are better) and most of them are very light: at most 64 bytes. – freakish Oct 19 '21 at 07:58
  • 1
    Doesn't work in .net 6 – Andrew Richesson Sep 06 '22 at 00:52
  • @AndrewRichesson: I'm not surprised, due to https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/5.0/binaryformatter-serialization-obsolete – Jon Skeet Sep 06 '22 at 06:37
3

Here's a dotnet 5.0 or later struct that can save and load the current state of a System.Random instance. This can surely be improved but it works as it should. Also, I'm pretty sure it only works with System.Random instances that have been created with a Seed parameter (System.Random(int Seed)). I noticed while debugging that using the parameterless constructor of System.Random yields an other type of ImplBase named XoshiroImpl that lacks seed support therefore making my code unusable.

This can be serialized with System.TEx

Net5CompatSeedImpl class: https://github.com/dotnet/runtime/blob/f7633f498a8be34bee739b240a0aa9ae6a660cd9/src/libraries/System.Private.CoreLib/src/System/Random.Net5CompatImpl.cs#L283

XoshiroImpl class: https://github.com/dotnet/runtime/blob/4017327955f1d8ddc43980eb1848c52fbb131dfc/src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro128StarStarImpl.cs

using System.Reflection;
public struct RandomState
{
    static RandomState()
    {
        ImplInfo = typeof(Random).GetField("_impl", BindingFlags.Instance | BindingFlags.NonPublic)!;
        PrngInfo = Type.GetType(Net5CompatSeedImplName)!.GetField("_prng", BindingFlags.Instance | BindingFlags.NonPublic)!;
        Type compatPrngType = Type.GetType(CompatPrngName)!;
        seedArrayInfo = compatPrngType.GetField(SeedArrayInfoName, BindingFlags.Instance | BindingFlags.NonPublic)!;
        inextInfo = compatPrngType.GetField(InextInfoName, BindingFlags.Instance | BindingFlags.NonPublic )!;
        inextpInfo = compatPrngType.GetField(InextpInfoName, BindingFlags.Instance | BindingFlags.NonPublic)!;
    }
    public const string CompatPrngName = "System.Random+CompatPrng";
    public const string Net5CompatSeedImplName = "System.Random+Net5CompatSeedImpl";
    public const string SeedArrayInfoName = "_seedArray";
    public const string InextInfoName = "_inext";
    public const string InextpInfoName = "_inextp";
    private static FieldInfo ImplInfo;
    private static FieldInfo PrngInfo;
    private static FieldInfo seedArrayInfo;
    private static FieldInfo inextInfo;
    private static FieldInfo inextpInfo;

    public int[] seedState { get; set; }
    public int inext { get; set; }
    public int inextp { get; set; }
    public static RandomState GetState(Random random)
    {
        object o = GetCompatPrng(random);
        RandomState state = new RandomState();
        state.seedState = (int[])seedArrayInfo.GetValue(o)!;
        state.inext = (int)inextInfo.GetValue(o)!;
        state.inextp = (int)inextpInfo.GetValue(o)!;
        return state;

    }
    //Random > Impl > CompatPrng
    public static object GetImpl(Random random) => ImplInfo.GetValueDirect(__makeref(random))!;
    public static object GetCompatPrng(object impl) => PrngInfo.GetValueDirect(__makeref(impl))!;
    public static object GetCompatPrng(Random random)
    {
        object impl = GetImpl(random);
        return PrngInfo.GetValueDirect(__makeref(impl))!;
    }
    public static void SetState(Random random, RandomState state)
    {
        object impl = GetImpl(random);
        TypedReference implref = __makeref(impl);

        object prng = PrngInfo.GetValueDirect(implref)!;

        seedArrayInfo.SetValue(prng, state.seedState);
        inextInfo.SetValue(prng, state.inext);
        inextpInfo.SetValue(prng, state.inextp);

        PrngInfo.SetValueDirect(implref, prng);
        //Testing. can be removed.
        /*object o2 = GetCompatPrng(impl);
        DehFwk.Debug.Log("orig: " + ((int[])seedArrayInfo.GetValue(prng)!).Length + "| new: " + ((int[])seedArrayInfo.GetValue(o2)!).Length + " vs " + state.seedState.Length);
        DehFwk.Debug.Log("orig: " + inextInfo.GetValue(prng)! + " " + "| new: " + inextInfo.GetValue(o2) + " vs " + state.inext);
        DehFwk.Debug.Log("orig: " + inextpInfo.GetValue(prng) + "| new: " + inextpInfo.GetValue(o2) + " vs " + state.inextp);*/

    }
}
Pat
  • 31
  • 2
1

Here's what I came up with in my static utility class:

//* Used for Getting and setting System.Random state *//
private static System.Reflection.FieldInfo[] randomFields;
private static System.Reflection.FieldInfo[] RandomFields { get { if (randomFields == null) {
            randomFields = new System.Reflection.FieldInfo[3];
            var t = typeof(System.Random);
            randomFields[0] = t.GetField("SeedArray", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            randomFields[1] = t.GetField("inext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            randomFields[2] = t.GetField("inextp", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        }
        return randomFields;
    } }
/// <summary>
/// Gets <see cref="System.Random"/> current state array and indexes with Reflection.
/// </summary>
/// <param name="rand"></param>
/// <returns></returns>
public static int[] GetSeedArray(this System.Random rand) {
    var state = new int[58];
    ((int[])RandomFields[0].GetValue(rand)).CopyTo(state, 0);
    state[56] = (int)RandomFields[1].GetValue(rand);
    state[57] = (int)RandomFields[2].GetValue(rand);
    return state;
}

/// <summary>
/// Restores saved <see cref="System.Random"/> state and indexes with Reflection. Use with caution.
/// </summary>
/// <param name="rand"></param>
/// <param name="seedArray"></param>
public static void SetSeedArray(this System.Random rand, int[] seedArray) {
    if (seedArray.Length != 56 + 2) return;

    Array.Copy(seedArray, ((int[])RandomFields[0].GetValue(rand)), 56);
    RandomFields[1].SetValue(rand, seedArray[56]);
    RandomFields[2].SetValue(rand, seedArray[57]);
}
zORg Alex
  • 372
  • 2
  • 15
1

To ensure the same initial sequence of random numbers upon new start (that's how OP tests it I guess) one can seed the new instance of Random with a number from previous Random.

I guess f(rnd) (random generator) is same random as f(f(rnd)). With latter you shouldn't be able to predict a number any way easier, nor the distribution should suffer. I would be very thankful if someone can prove me wrong.

So instead of saving the complete state with ugly hacks I propose to simply store the last number before entering a critical section and use that number as a seed later for a critical section.

Below code produce same result as @Jon's answer, the numbers of second and third output should be identical:

var rnd = new Random();
Test("Before", rnd);

// only this number is needed to "restore" sequence for critical section
var seed = rnd.Next();
rnd = new(seed);
Test("Critical section", rnd);

rnd = new(seed);
Test("Again critical section", rnd);

static void Test(string header, Random rnd)
{
    Console.WriteLine(header);
    for (int i = 0; i < 5; i++)
        Console.WriteLine(rnd.Next(100));
}
Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Thank you for this, I was trying a lot of the other solutions to persist a Random instance to a file to load at a later point and have it consistent. All the others suffered from complexity, out of date, or simply not working anymore. This was perfect. – Moo-Juice Feb 23 '23 at 16:29
0

I'm not a C# guy, so I don't have the resources to see the sourcecode, but the seed must be stored in the class. From its documentation the class is not sealed, so you should be able to make a subclass. In this subclass, you can create a function to return the current seed. On shutdown of your app, you can save the seed (file? database? monkeys with good memory?) and then load it back up when you start your new app.

This has the added benefit of allowing you to restore from any previously saved point, for backup purposes or something.

corsiKa
  • 81,495
  • 25
  • 153
  • 204
  • Do you have reason to believe the seed is part of the public (or protected) API of the class? Inheritance won't help if the seed is a private implementation detail, derived classes will not have access. – Anthony Pegram Nov 18 '11 at 20:54
  • @Anthony I stated I don't have the source code. I suppose I could have been explicit in saying I do not have conclusive evidence to support that, so no, I do not. To me, it seems like something logical to include in your protected API. – corsiKa Nov 18 '11 at 21:00
  • i will look into this, since i don't like what i see in the saved file from the serialization – Fredou Nov 18 '11 at 21:01
  • @Fredou with some investigation, you could probably combine mine and Jon's methods: use the serialization mechanism to extract the seed instead of subclassing, save it, and load it up as normal. – corsiKa Nov 18 '11 at 21:08
  • That depends on what you mean by "seed". Typically seed means a value that is used to initialize the generator. This value is extremely unlikely to be kept inside any prng. The reason is simply that it is not needed, it is only used to initialize the state which is then mixed in each iteration. But you don't need seed, you only need the current state. Since those algorithms are deterministic then they can't depend on anything else. – freakish Oct 19 '21 at 07:48
  • @freakish What I meant by seed is exactly what developers of PRNGs mean by seed, which is the current state. You're possibly confusing "seed" with "initial seed". In the intervening 10 years since I wrote this, it appears Microsoft has made their source code available and I can confirm that both [C# random](https://referencesource.microsoft.com/#mscorlib/system/random.cs) and [Java random](https://github.com/openjdk/jdk15u-dev/blob/f35be8441911e38312b77378627f65026c8de589/src/java.base/share/classes/java/util/Random.java) refer to the internal state as the seed. – corsiKa Oct 19 '21 at 18:48
  • @corsiKa well, you refer to developers of PRNG of both C# and Java, yet C is about 20 years older than both and the term "seed" is used only for initialization in rand/srand: http://mirror.fsf.org/pmon2000/3.x/src/lib/libc/rand.c In fact this is confirmed by this wiki entry (if it matters): https://en.wikipedia.org/wiki/Random_see and that's how I always used the term. Modern papers on prngs seem to follow this as well, from what I've seen at least. Anyway it is a naming issue, not that important. – freakish Oct 19 '21 at 20:05
  • @freakish I'm not sure if you read the comments on the function `srand` that you linked, but the phrase `Initializes the seed.` makes it clear they refer to `next int` as the seed to be initialized and is entirely in line with what I would expect, even if the member variable is named `next_int` it is still a seed (and should actually be named `seed` since the value `next_int` is not the actual next int returned... – corsiKa Oct 20 '21 at 03:06
  • here is the easily browsable source code https://source.dot.net/#System.Private.CoreLib/Random.cs,bb77e610694e64ca – pm100 Apr 10 '22 at 00:58