When we think about programming, we usually think about well-intentioned systematic instructions executing when and how we intended. In a digital world of One’s and Zero’s, the idea of randomness is madness. In the real world, randomness is a necessary part of our lives.
When programming, you may be required to generate random values to mimic the real-world system you are trying to emulate. Before we get to code, it is essential to define what we mean by randomness because it can have variations that can lead to consequences we never intended.
What Is Randomness?
The idea of randomness may seem intuitive, but there are two kinds of randomness you should consider when looking to add it to your applications:
- Predictability
- Uniformity
If you look at Merriam-Webster’s definition of random, you’ll get an explanation mentioning both concepts. The first being about predictability.
lacking a definite plan, purpose, or pattern–Merriam-Webster
In this case, given our previous set of values, can I predict with high confidence what the next value will be? If I can not, then I have a random process.
Uniformity may be more critical when wanting randomness.
relating to, having, or being elements or events with [a] definite probability of occurrence –Merriam-Webster
Uniformity doesn’t look at any single value, but the complete set of our random values. If our collection has a general distribution, then we know our randomness is even and has no biases that could potentially be hurting us.
The following post will look at several ways to generate random values in .NET Core from the perspective of predictability and uniformity. You will also see samples in C# and .NET Core that you can use for your particular use cases.
Random Class
The .NET Framework has shipped with the Random
class since the initial releases. The Random
class is still here in .NET Core and performs very similarly.
When generating values, you have the option between several outputs:
1.Integers (int
)
1.Bytes (byte
)
1.Doubles (double
)
Accessing values occurs through methods Next
, NextBytes
, and NextDouble
appropriately.
Generate Using Random (Vanilla)
Let’s generate a basic random integer with a C# sample.
var random = new Random();
var value = random.Next();
return value;
With the following output
Random Using Next: 303739907
This value is random, so your number will likely be different.
The Random
class can also take a seed
parameter that can impact the number generated by our class.
var random = new Random(seed:1);
var value = random.Next();
return value;
But what is the seed, and what is it doing to our random values?
The Microsoft Documentation explicitly states: “A number used to calculate a starting value for the pseudo-random number sequence.”
If you read that definition carefully, you may notice some words that indicate the presence of predictability, one of our randomness concepts.
The Problem With Random (the class)
While not generally an issue, the class is predictable. Given similar circumstances, we could generate the same random numbers every time with the Random
class.
On the Microsoft Documentation site, they show an example where the random class generates the same values when using the same seed
Random numbers from a Random object with seed = 456: 2044805024 1323311594 1087799997 1907260840 179380355 120870348 0.21988117 0.21026556 0.39236514 0.42420498 0.24102703 0.47310170
Random numbers from a Random object with seed = 456: 2044805024 1323311594 1087799997 1907260840 179380355 120870348 0.21988117 0.21026556 0.39236514 0.42420498 0.24102703 0.47310170
While the general distribution may look random (uniformity), this randomness is predictable.
Providing an identical seed value to different Random objects causes each instance to produce identical sequences of random numbers. Microsoft Documentation
Well, what if we want unpredictable randomness?
RandomNumberGenerator & Cryptographic Randomness
.NET Core ships with a RandomNumberGenerator
. The specific implementation of the random number generator is RNGCryptoServiceProvider
. This class generates cryptographically secure values. In other words, the values it produces are not predictable by you or anyone. They are truly random, but that comes at a cost, as you’ll see.
The most significant difference is that the RandomNumberGenerator
and Random
is that the former can only generate random bytes. Luckily, bytes are the building blocks of all C# types. Let’s look at an example where we create a random integer that is cryptographically secure.
var random = RandomNumberGenerator.Create();
var bytes = new byte[sizeof(int)]; // 4 bytes
random.GetNonZeroBytes(bytes);
var result = BitConverter.ToInt32(bytes);
The example above uses the sizeof
operator to get the size of our target type int
, which is 4 bytes. Next, we fill a byte[]
with 4 bytes. Finally, we use the BitConverter
class to convert our bytes to our target type of int
. Try this technique to any kind that BitConverter
supports.
I’ve also written a simple static method that gives you Next
functionality similar to the one in the Random
class.
static int Next(this RandomNumberGenerator generator, int min, int max)
{
// match Next of Random
// where max is exclusive
max = max - 1;
var bytes = new byte[sizeof(int)]; // 4 bytes
generator.GetNonZeroBytes(bytes);
var val = BitConverter.ToInt32(bytes);
// constrain our values to between our min and max
// https://stackoverflow.com/a/3057867/86411
var result = ((val - min) % (max - min + 1) + (max - min + 1)) % (max - min + 1) + min;
return result;
}
Update In .NET Core 3.0
As Kevin Jones pointed out in a Twitter conversation. The implementation I have above has a slight bias due to the modulus operator. The fourth value generated seems to trend lower. This can be an issue when talking about predictability.
Taking the code from Stack Overflow and generating 100 million numbers [0, 100), it generates this. Interesting that every 4th number is consistently smaller. pic.twitter.com/XDYH9rKjNq
— Kevin Jones 🏳️🌈🏒⚾️ (@vcsjones) October 28, 2019
Luckily, Kevin is a cryptographic genius and has submitted an implementation into .NET Core 3.0 that removes the bias. Read his post about the RandomNumberGenerator
.
The built-in corefx version does not demonstrate that bias.
— Kevin Jones 🏳️🌈🏒⚾️ (@vcsjones) October 28, 2019
Anyway... use the one in corefx or steal the code if you aren't on .NET Core 3 yet :-). pic.twitter.com/6KGfKdK87Q
As he says in hist post. You should be able to generate random integers with no bias in .Net Core 3.0 as follows. Note, the method of GetInt32
is a static method off of RandomNumberGenerator
and not an instance method.
var min = 1;
var max = 1_000;
var randomNumber = RandomNumberGenerator.GetInt32(min, max);
With an implementation as follows:
public static int GetInt32(int fromInclusive, int toExclusive)
{
if (fromInclusive >= toExclusive)
throw new ArgumentException(SR.Argument_InvalidRandomRange);
// The total possible range is [0, 4,294,967,295).
// Subtract one to account for zero being an actual possibility.
uint range = (uint)toExclusive - (uint)fromInclusive - 1;
// If there is only one possible choice, nothing random will actually happen, so return
// the only possibility.
if (range == 0)
{
return fromInclusive;
}
// Create a mask for the bits that we care about for the range. The other bits will be
// masked away.
uint mask = range;
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
mask |= mask >> 8;
mask |= mask >> 16;
Span<uint> resultSpan = stackalloc uint[1];
uint result;
do
{
RandomNumberGeneratorImplementation.FillSpan(MemoryMarshal.AsBytes(resultSpan));
result = mask & resultSpan[0];
}
while (result > range);
return (int)result + fromInclusive;
}
All credit goes to Kevin Jones, for pointing this out and improving the .NET Ecosystem. Check out his full pull request to the .NET Core 3.0 code base.
The Problem With RandomNumberGenerator
Performance is the high cost of gaining cryptographic randomness. When generating 10,000 random values, the difference is magnitudes more expensive.
var random = new Random();
using var crypto = RandomNumberGenerator.Create();
var min = 1;
var max = 10000;
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 10000; i++)
{
random.Next(min, max);
}
watch.Stop();
Console.WriteLine($"Random took {watch.Elapsed.TotalMilliseconds} ms");
watch.Restart();
for (int i = 0; i < 10000; i++)
{
crypto.Next(min, max);
}
watch.Stop();
Console.WriteLine($"RandomNumberGenerator took {watch.Elapsed.TotalMilliseconds} ms");
The results speak for themselves, showing RandomNumberGenerator
trailing Random
.
Random | RandomNumberGenerator |
---|---|
0.4052 ms | 90.7277 ms |
The performance issue may not be a deal-breaker for you, and you should evaluate the cost of using RandomNumberGenerator
based on your specific needs.
What About Uniformity
Well, this is where things get interesting. I hypothesized that a cryptographic implementation would produce more uniform values over time. Given a range of possible values, they would all have an equal probability if being generated.
private static void GenerateComparisonDistributions()
{
var random = new Random();
using var crypto = RandomNumberGenerator.Create();
var min = 1;
var max = 10000;
const string filename = "random_distribution.csv";
if (File.Exists(filename))
{
File.Delete(filename);
}
using var file = File.OpenWrite(filename);
var encoding = new UTF8Encoding(true);
file.Write(encoding.GetBytes("random,cryptography\n"));
for(var i =1; i < 10000; i++)
{
var row = encoding.GetBytes($"{random.Next(min, max)},{crypto.Next(min, max)}\n");
file.Write(row);
}
file.Flush();
}
The experiment I ran generated 10,000 random values between 1 and 10,000. If the series averages 5,000, then we know we have a uniform distribution. So what was the outcome?
Random | RandomNumberGenerator |
---|---|
4994.29863 | 4973.205721 |
Remember, the distribution of both generator values are random, but both averages are converging on 5,000 with an insignificant difference.
Random
has the exact random uniformity asRandomNumberGenerator
.
If you are not concerned about predictability, you should use Random
. It is significantly more performance, with no compromise in uniformity.
Conclusion
Randomness in a non-random domain like computer programming is a difficult problem to solve. Luckily, we have the fine folks working on the .NET Framework giving us tools to make it easier.
Remember to consider both predictability and uniformity when choosing your random implementation, as it can make a big difference in the areas of security and performance.
If you need to generate a cryptographically secure (unpredictable) value, remember that the combination of RandomNumberGenerator
and BitConverter
can go a long way to help you. Ultimately, both Random
and RandomNumberGenerator
will give you the same uniform output.