RNGCryptoServiceProvider – 更快地生成范围内的数字并保留分布?

首先,我正在打电话,所以请原谅糟糕的格式!

我现在已经做了很多搜索,但没有找到明确的答案。 如果没有一个,那么公平,但我确信有人比我必须有一个好答案更聪明!

我正在使用RNG加密提供程序以真正天真的方式生成范围内的数字:

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

当范围足够大以至于获得结果的可能性很大时,这是很好的,但是今天早些时候我遇到的范围足够小(在10,000个数字内)可能需要一个年龄。

所以我一直在努力想出一个更好的方法来实现合理的分配,但会更快。 但是现在我正在深入学习数学和统计数据,而我在学校根本就没有这样做,或者至少如果我这样做,我已经忘记了这一切!

我的想法是:

  • 获得最小和最大的最高设置位位置,例如,对于4,它将是3,对于17,它将是5
  • 从prng中选择至少包含高位的字节数,例如,在这种情况下为8位
  • 查看是否设置了允许范围(3-5)中的任何高位
  • 如果是,请将其转换为包含高位的数字
  • 如果该数字在最小值和最大值之间,则返回。
  • 如果以前的任何测试失败,请重新开始。

就像我说的那样,这可能非常幼稚,但我相信它会在比目前的实施更快的范围内返回一个匹配。 我现在不在电脑前所以无法测试,明天早上英国时间会这样做。

但当然速度并不是我唯一关注的问题,否则我只会使用随机 (如果有人会非常友好,那么在那里需要几个刻度线来正确格式化 – 它们不在Android键盘上!)。

我对上述方法的最大担忧是,我总是丢掉由prng生成的最多7位,这看起来很糟糕。 我想到了将它们考虑在内的方法(例如一个简单的添加),但它们似乎非常不科学的黑客!

我知道mod技巧,你只需要生成一个序列,但我也知道它的弱点。

这是死路一条吗? 最终,如果最好的解决方案是坚持当前的实现,我会觉得必须有更好的方法!

Stephen Toub和Shawn Farkas在MSDN上共同撰写了一篇名为Tales From The CryptoRandom的优秀文章,如果您正在尝试使用RNGCryptoServiceProviders ,您一定要阅读

在它中,它们提供了一个inheritance自System.Random的实现(它包含您正在寻找的漂亮的范围随机方法),但是它们的实现不使用伪随机数,而是使用RNGCryptoServiceProvider 。

他实现Next(min,max)方法的方法如下:

 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)); } } } 

选择实施的原因以及关于随机性丧失的详细分析以及他们为生成高质量随机数而采取的步骤在他们的文章中 。

线程安全缓冲CryptoRandom

我编写了一个Stephen类的扩展实现,它使用了一个随机缓冲区,以最大限度地减少调用GetBytes()的任何开销。 我的实现还使用同步来提供线程安全性,从而可以在所有线程之间共享实例以充分利用缓冲区。

我为一个非常具体的场景写了这个,所以你当然应该根据应用程序的特定争用和并发属性来描述是否对你有意义。 如果你不想检查它,我把代码放在github上。

Threadsafe基于Stephen Toub和Shawn Farkas的实现缓冲了CryptoRandom

当我写它(几年前)时,我似乎也做了一些分析

 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 | |---------------------|------------------------------------| 

请注意,theese测量仅描绘非常具体的非现实场景,并且仅应用于指导,测量您的场景以获得正确的结果。

您可以一次生成更多字节,但开销非常小。 RNGCrptoService的主要开销是调用本身来填充字节。

虽然你可能会丢弃未使用的字节,但我会给它一个镜头,因为我已经从这个和模数方法(你没有使用)获得了非常好的速度。

 int vSize = 20*4; byte[] vBytes = new byte[vSize]; RNG.GetBytes(vBytes); int vResult = 0; int vLocation = 0; while(vResult < min || vResult > max) { vLocation += 4; vLocation = vLocation % vSize; if(vLocation == 0) RNG.GetBytes(vBytes); vResult = BitConverter.ToInt32(vBytes, vLocation); } 

你可以做的另一件事是比较你在哪里按位思考。 但是,我会关注范围是否适合字节,短字,整数或长整数。 然后,您可以通过该类型的最大值来模拟int结果(给出较低的位数)。

 //We want a short, so we change the location increment and we modulo the result. int vSize = 20*4; byte[] vBytes = new byte[vSize]; RNG.GetBytes(vBytes); int vResult = 0; int vLocation = 0; while(vResult < min || vResult > max) { vLocation += 2; vLocation = vLocation % vSize; if(vLocation == 0) RNG.GetBytes(vBytes); vResult = BitConverter.ToInt32(vBytes, vLocation) % 32768; } 

如果使用while循环,这将会很慢,并且基于未知的迭代次数。

您可以使用模运算符(%) 在第一次尝试时计算它

但是,如果我们用模数来挤压结果,我们会立即在概率分布中产生不平衡。

这意味着如果我们只关心速度而不是生成数字的概率随机性 ,则可以应用这种方法

这是一个RNG实用程序,可以满足您的需求:

 using System; using System.Security.Cryptography; static class RNGUtil { ///  is greater than . public static int Next(int min, int max) { if (min > max) throw new ArgumentOutOfRangeException(nameof(min)); if (min == max) return min; using (var rng = new RNGCryptoServiceProvider()) { var data = new byte[4]; rng.GetBytes(data); int generatedValue = Math.Abs(BitConverter.ToInt32(data, startIndex: 0)); int diff = max - min; int mod = generatedValue % diff; int normalizedNumber = min + mod; return normalizedNumber; } } } 

在这种情况下, RNGUtil.Next(-5, 20)将获取范围-5..19内的任意数字

一点测试:

 var list = new LinkedList(); for (int i = 0; i < 10000; i++) { int next = RNGUtil.Next(-5, 20); list.AddLast(next); } bool firstNumber = true; foreach (int x in list.Distinct().OrderBy(x => x)) { if (!firstNumber) Console.Out.Write(", "); Console.Out.Write(x); firstNumber = false; } 

输出: -5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 ,17,18,19

以下是@ Andrey-WD上面回答的改编,但不同之处在于您只是发送了一个已经生成的随机数(在这种情况下, ulong可以更改为uint )。 这是非常有效的,当你需要一个范围内的多个随机数时,你可以简单地通过RNGCryptoServiceProvider (或者其他任何东西,如果符合你的需要,使用Random )生成这样的数字数组。 当我需要在一个范围内生成多个随机数时,我肯定会更加高效。 你所需要的只是随机麻木的东西来喂养这个function。 请参阅上面关于@ Andrey-WD的回答的说明,我很好奇为什么其他人没有做这种简单的模数路线,不需要多次迭代。 如果确实存在多次迭代路由的必要原因,我会很高兴听到它。

  public static int GetRandomNumber(int min, int max, ulong randomNum) { if (min > max) throw new ArgumentOutOfRangeException(nameof(min)); if (min == max) return min; //var rng = new RNGCryptoServiceProvider(); //byte[] data = new byte[4]; //rng.GetBytes(data); //int generatedValue = Math.Abs(BitConverter.ToInt32(data, startIndex: 0)); int diff = max - min; int mod = (int)(randomNum % (ulong)diff); // generatedValue % diff; int normalizedNumber = min + mod; return normalizedNumber; } 

以下是如何有效地获得一组干净的随机数。 我喜欢这样干净地封装获取随机数的方法,使用它的代码然后不必在每次迭代时使用字节转换混乱以使用BitConverter获得int或long。 我还假设通过将字节单数转换为数组类型来获得性能。

  public static ulong[] GetRandomLongArray(int length) { if (length < 0) throw new ArgumentOutOfRangeException(nameof(length)); ulong[] arr = new ulong[length]; if (length > 0) { // if they want 0, why 'throw' a fit, just give it to them ;) byte[] rndByteArr = new byte[length * sizeof(ulong)]; var rnd = new RNGCryptoServiceProvider(); rnd.GetBytes(rndByteArr); Buffer.BlockCopy(rndByteArr, 0, arr, 0, rndByteArr.Length); } return arr; } 

用法:

  ulong[] randomNums = GetRandomLongArray(100); for (int i = 0; i < 20; i++) { ulong randNum = randomNums[i]; int val = GetRandomNumber(10, 30, randNum); // get a rand num between 10 - 30 WriteLine(val); }