I have a requirement to generate a uniform distribution of cryptographically secure random numbers. To generate these numbers I only know of the RNGCryptoServiceProvider class, that has the method GetBytes(byte[] data). This is good, but I have to then change this discrete uniform distribution ranging from 0 - 256 to a different discrete uniform distribution ranging from 0 - limit.
And so I thought of three ways to do this. The first two ways were using % and /, which don't work if limit is not a divisor of 256. As this leads to a non-uniform distribution. However filtering all numbers that a not less than the limit works and gives a uniform distribution, however has terrible performance problems when limit is a small number.
I'm currently using:
private static IEnumerable<object> Cycle(object value=null)
{
while (true)
{
yield return value;
}
}
private static IEnumerable<byte> RandomBytes(int chunkSize)
{
return Cycle().SelectMany(unused =>
{
var b = new byte[chunkSize];
new RNGCryptoServiceProvider().GetBytes(b);
return b;
});
}
private static IEnumerable<int> RandomFilter(int chunkSize, int limit)
{
return RandomBytes(chunkSize).Where(n => n < limit).Select(n => (int)n);
}
Since I tried using % and /, below is the code that I used to check if there is a uniform distribution, and those two solutions.
private static IEnumerable<int> RandomModulo(int chunkSize, int limit)
{
return RandomBytes(chunkSize).Select(n => n % limit);
}
private static IEnumerable<int> RandomResize(int chunkSize, int limit)
{
var denom = 256.0/limit;
return RandomBytes(chunkSize).Select(n => (int)(n / denom));
}
private static void TestDistribution(int limit, int amount, int chunkSize, Func<int, int, IEnumerable<int>> fn)
{
var counter = new Dictionary<int, int>();
foreach (var i in Enumerable.Range(0, limit))
{
counter[i] = 0;
}
foreach (var i in fn(chunkSize, limit).Take(amount))
{
counter[i] += 1;
}
Console.WriteLine($"{limit} {amount} {chunkSize} {counter.Select(kv => kv.Value).Aggregate(0, (acc, x) => acc + x)}");
Console.WriteLine(string.Join(", ", from kv in counter select $"{kv.Key}"));
Console.WriteLine(string.Join(", ", from kv in counter select $"{kv.Value}"));
Console.WriteLine();
}
private static void Main()
{
TestDistribution(45, 100000000, 1000, RandomFilter);
TestDistribution(32, 100000000, 1000, RandomFilter);
Console.WriteLine();
TestDistribution(45, 100000000, 1000, RandomModulo);
TestDistribution(32, 100000000, 1000, RandomModulo);
Console.WriteLine();
TestDistribution(45, 100000000, 1000, RandomResize);
TestDistribution(32, 100000000, 1000, RandomResize);
Console.ReadLine();
}
Randomis a PRNG and has low entropy. \$\endgroup\$RNGCryptoServiceProviderimplementsIDisposableand therefore should be wrapped in ausingconstruct to ensure a proper deterministic lifetime. \$\endgroup\$