Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RNGCryptoServiceProvider - generate number in a range faster and retain distribution?

I'm using the RNG crypto provider to generate numbers in a range the truly naive way:

byte[] bytes = new byte[4]; int result = 0; while(result < min || result > max) {    RNG.GetBytes(bytes);    result = BitConverter.ToInt32(bytes); }   

This is great when the range is wide enough such that there is a decent chance of getting a result, but earlier today I hit a scenario where the range is sufficiently small (within 10,000 numbers) that it can take an age.

So I've been trying to think of a better way that will achieve a decent distribution but will be faster. But now I'm getting into deeper maths and statistics that I simply didn't do at school, or at least if I did I have forgotten it all!

My idea is:

  • get the highest set bit positions of the min and max, e.g. for 4 it would be 3 and for 17 it would be 5
  • select a number of bytes from the prng that could contain at least the high bits, e.g.1 in this case for 8 bits
  • see if any of the upper bits in the allowed range (3-5) are set
  • if yes, turn that into a number up to and including the high bit
  • if that number is between min and max, return.
  • if any of the previous tests fail, start again.

Like I say, this could be exceedingly naive, but I am sure it will return a match in a narrow range faster than the current implementation. I'm not in front of a computer at the moment so can't test, will be doing that tomorrow morning UK time.

But of course speed isn't my only concern, otherwise I would just use Random (needs a couple of tick marks there to format correctly if someone would be kind enough - they're not on the Android keyboard!).

The biggest concern I have with the above approach is that I am always throwing away up to 7 bits that were generated by the prng, which seems bad. I thought of ways to factor them in (e.g. a simple addition) but they seem terribly unscientific hacks!

I know about the mod trick, where you only have to generate one sequence, but I also know about its weaknesses.

Is this a dead end? Ultimately if the best solution is going to be to stick with the current implementation I will, I just feel that there must be a better way!

like image 285
Andras Zoltan Avatar asked Jun 09 '11 20:06

Andras Zoltan


Video Answer


1 Answers

Stephen Toub and Shawn Farkas has co-written an excellent article on MSDN called Tales From The CryptoRandom that you should definitely read if you're experimenting with RNGCryptoServiceProviders

In it they provide an implementation that inherits from System.Random (which contains the nice range-random method that you're looking for) but instead of using pseudo random numbers their implementation uses the RNGCryptoServiceProvider.

The way he has implemented the Next(min, max) method is as follows:

public override Int32 Next(Int32 minValue, Int32 maxValue) {     if (minValue > maxValue)          throw new ArgumentOutOfRangeException("minValue");     if (minValue == maxValue) return minValue;     Int64 diff = maxValue - minValue;     while (true)     {         _rng.GetBytes(_uint32Buffer);         UInt32 rand = BitConverter.ToUInt32(_uint32Buffer, 0);          Int64 max = (1 + (Int64)UInt32.MaxValue);         Int64 remainder = max % diff;         if (rand < max - remainder)         {             return (Int32)(minValue + (rand % diff));         }     } } 

The reasoning for the choice of implementation as well as a detailed analysis about loss of randomness and what steps they are taking to produce high-quality random numbers is in their article.

Thread safe bufferred CryptoRandom

I've written an extended implementation of Stephen's class which utilized a random buffer in order to minimize any overhead of calling out to GetBytes(). My implementation also uses synchronization to provide thread safety, making it possible to share the instance between all your threads to make full use of the buffer.

I wrote this for a very specific scenario so you should of course profile whether or not is makes sense for you given the specific contention and concurrency attributes of your application. I threw the code up on github if you wan't to check it out.

Threadsafe buffered CryptoRandom based on Stephen Toub and Shawn Farkas' implementation

When I wrote it (a couple of years back) I seem to have done some profiling as well

Results produced by calling Next() 1 000 000 times on my machine (dual core 3Ghz)  System.Random completed in 20.4993 ms (avg 0 ms) (first: 0.3454 ms) CryptoRandom with pool completed in 132.2408 ms (avg 0.0001 ms) (first: 0.025 ms) CryptoRandom without pool completed in 2 sec 587.708 ms (avg 0.0025 ms) (first: 1.4142 ms)  |---------------------|------------------------------------| | Implementation      | Slowdown compared to System.Random | |---------------------|------------------------------------| | System.Random       | 0                                  | | CryptoRand w pool   | 6,6x                               | | CryptoRand w/o pool | 19,5x                              | |---------------------|------------------------------------| 

Please note that theese measurements only profile a very specific non-real-world scenario and should only be used for guidance, measure your scenario for proper results.

like image 121
Markus Olsson Avatar answered Oct 05 '22 15:10

Markus Olsson