2

I have encountered this weird behaviour, which is probably best described by a small example:

Random R = new Random();

for (int i = 0; i < 10_000; i++)
{
    double d = R.NextDouble() * uint.MaxValue;
}

Now, the last digit of d before the decimal mark is always even, i.e. int r = (int) (d % 10) is always 0, 2, 4, 6, or 8. There are odd digits on either side, though.

I suspected that multiplying with uint.MaxValue (2^32 - 1) could force some rounding error in the last digits, but since double has more than 50 bits of precision, this should beat uint with about 20 bits to spare after the separator. This behaviour also occurs if I explicitly store uint.MaxValue as a double before the multiplication and use that instead.

Can someone shed any light on this?

MinosIllyrien
  • 330
  • 1
  • 9
  • 2
    The precision of `double` isn't relevant; the lack of precision of `Random` is. It doesn't attempt to use all bits; not by a long shot. Internally it's just multiplying a "random" `int`. – Jeroen Mostert Aug 06 '19 at 09:56
  • 1
    I think this must be something specifically to do with the implementation of Random.NextDouble, because this doesn't happen with other RNGs (e.g. one based on XorShift) – Matthew Watson Aug 06 '19 at 09:56

1 Answers1

4

This is a deficiency in the .Net Random class.

If you inspect the source code you will see the following comment in the implementation of the private method GetSampleForLargeRange():

      // The distribution of double value returned by Sample 
      // is not distributed well enough for a large range.
      // If we use Sample for a range [Int32.MinValue..Int32.MaxValue)
      // We will end up getting even numbers only.

This is used in the implementation of Next():

public virtual int Next(int minValue, int maxValue) {
  if (minValue>maxValue) {
      throw new ArgumentOutOfRangeException("minValue",Environment.GetResourceString("Argument_MinMaxValue", "minValue", "maxValue"));
  }
  Contract.EndContractBlock();

  long range = (long)maxValue-minValue;
  if( range <= (long)Int32.MaxValue) {  
      return ((int)(Sample() * range) + minValue);
  }          
  else { 
      return (int)((long)(GetSampleForLargeRange() * range) + minValue);
  }
}

But it is NOT used for the values returned from NextDouble() (which just returns the value returned from Sample().

So the answer is that NextDouble() is not well-distributed.


You can use RNGCryptoServiceProvider to generate better random numbers, but it's a bit of a fiddle to create the double. From this answer:

static void Main()
{
    var R = new RNGCryptoServiceProvider();
    var bytes = new Byte[8];

    for (int i = 0; i < 10_000; i++)
    {
        R.GetBytes(bytes);
        var ul = BitConverter.ToUInt64(bytes, 0) / (1 << 11);
        var d  = ul / (double)(1UL << 53);

        d *= uint.MaxValue;

        Console.WriteLine(d);
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Thank you. I didn't know that NextDouble() didn't return random values in the entire possible span. I only needed some approximately random numbers, so I just multiplied an extra random number onto the first one which solved my problem with the even digits. The implementation of NextDouble() seems a bit sketchy though. – MinosIllyrien Aug 06 '19 at 10:51