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

And*_*tan 27 c# security random

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

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

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

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

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

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

我的想法是:

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

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

但当然速度并不是我唯一关心的问题,否则我只会使用随机(如果有人愿意的话,那里需要一些刻度线来正确格式化 - 它们不在Android键盘上!).

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

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

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

Mar*_*son 53

斯蒂芬Toub和Shawn Farkas医生就共同撰写一篇优秀文章在MSDN上称为传说从CryptoRandom,你一定要读,如果您的实验RNGCryptoServiceProviders

在它中,它们提供了一个继承自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));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

线程安全缓冲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                              |
|---------------------|------------------------------------|
Run Code Online (Sandbox Code Playgroud)

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

  • 可以在此处找到MSDN文章:[MSDNMagazineSeptember2007en-us.chm](http://download.microsoft.com/download/3/A/7/3A7FA450-1F33-41F7-9E6D-3AA95B5A6AEA/MSDNMagazineSeptember2007en-us.chm) ,以CHM形式.(专栏,.Net事项:来自CryptoRandom的故事) (4认同)
  • 请注意,`max`参数在这里有一个奇怪的名称,因为`min`是包含的,而'max`是独占的.所以对于掷骰子你必须要求范围[1,7] - 即`max = 7`而不是`max = 6`. (2认同)

And*_*yWD 5

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

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

但是,如果我们用模压缩结果,我们会立即在概率分布中造成不平衡。

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

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

using System;
using System.Security.Cryptography;

static class RNGUtil
{
    /// <exception cref="ArgumentOutOfRangeException"><paramref name="min" /> is greater than <paramref name="max" />.</exception>
    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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

一个小测试:

var list = new LinkedList<int>();

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;
}
Run Code Online (Sandbox Code Playgroud)

输出: -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

  • 由于@MarkusOlsson 链接的文章现在已死,您必须使用时间机器:https://web.archive.org/web/20090304194122/http://msdn.microsoft.com:80/en-us/magazine /cc163367.aspx 这应该可以解释为什么您在此处给出的实现需要“while”循环。重要的不仅是我们得到*快*的结果,而且每个整数出现的机会均等。如果你用模压缩结果,你会立即在概率分布中造成不平衡。 (3认同)
  • @AndrasZoltan 代替文章打开的页面包含指向文章的 doanloadable .chm 版本的链接。事实上,他们几乎每篇文章都有一个链接。它按年/月组织。只需向下滚动即可查看。http://download.microsoft.com/download/3/A/7/3A7FA450-1F33-41F7-9E6D-3AA95B5A6AEA/MSDNMagazineSeptember2007en-us.chm (2认同)