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

时间:2011-06-09 20:58:42

标签: c# security random

首先我在打电话,所以请原谅格式不佳!

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

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

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

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

因此,我一直在努力想出一种更好的方式来实现合理的分销,但会更快。但现在我正在深入研究我在学校里没有做的更深入的数学和统计数据,或者至少我是否已经忘记了这一切!

我的想法是:

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

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

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

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

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

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

5 个答案:

答案 0 :(得分:46)

Stephen Toub和Shawn Farkas在MSDN上共同撰写了一篇名为Tales From The 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));
        }
    }
}

选择实施的原因以及关于随机性丢失的详细分析以及他们为产生高质量随机数而采取的步骤是their article

线程安全缓冲区CryptoRandom

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

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

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

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

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测量仅描述非常具体的非现实场景,并且只应用于指导,测量您的场景以获得正确的结果。

答案 1 :(得分:3)

您可以一次性生成更多字节,以获得非常小的开销。 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;
}

答案 2 :(得分:1)

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

您可以使用modulo operator (%) 第一次尝试计算

  

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

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

这是一个可满足您需求的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;
        }
    }
}

在这种情况下,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;
}
输出
-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

答案 3 :(得分:0)

以下是对@Andrey-WD's answer above的修改,但区别在于您只是发送了一个已生成的随机数(在这种情况下为ulong,可以更改为uint })。这是非常有效的,当你需要一个范围内的多个随机数时,你可以通过RNGCryptoServiceProvider(或者其他任何东西,甚至Random生成这样的数字数组,如果符合你的需要)。当我需要在一个范围内生成多个随机数时,我肯定会更加高效。你需要的只是随机麻木的东西来喂养这个功能。请参阅上面关于@ 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);
        }

答案 4 :(得分:0)

让我说说随机整数生成算法,就其平均使用的随机位数而言,它们是“最佳”的。在本文的其余部分,我们将假定我们有一个“真实的”随机发生器,可以产生无偏且独立的随​​机位。 (这里,一个随机的“字节”将是8个随机位的块。)

1976年,DE Knuth和AC Yao表明,任何仅使用随机位以给定概率生成随机整数的算法都可以表示为二叉树,其中随机位指示遍历树和每片叶子的方式(端点)对应于结果。 Knuth和Yao指出,任何用于均匀地在[0, n)中生成整数的最优二叉树算法,都需要至少 log2(n)位和最多log2(n) + 2位平均。 (因此,即使 optimum 算法也有可能“浪费”位。)有关最佳算法的示例,请参见下文。

但是,任何最优整数生成器(也都是无偏)通常会在最坏的情况下永远运行,如Knuth和Yao所示。回到二叉树,n个结果标签中的每一个都留在二叉树中,以便[0,n)中的每个整数都可以1 / n的概率出现。但是,如果1 / n具有不间断的二进制扩展(如果n不是2的幂,就会是这种情况),那么该二进制树必然是其中一个-

  • 具有“无限”的深度,或者
  • 在树的末端
  • 包括“拒绝”叶子,

无论哪种情况,即使平均使用很少的随机位,该算法也将在最坏的情况下永远运行。 (另一方面,当n为2的幂时,最佳二叉树将没有拒绝节点,并且在返回结果之前恰好需要n位,因此不会浪费任何位。)快速骰子滚子是使用“拒绝”事件以确保其无偏的算法示例;请参阅下面的代码中的注释。

因此,通常,随机整数生成器可以是 无偏的恒定时间(或什至都不是),。 >并且二叉树概念表明,一般而言,没有办法在不引入偏差的情况下“修复”不确定运行时间的最坏情况。例如,模减少(例如rand() % n)等效于二叉树,其中拒绝叶子用标记的结果替换-但是由于拒绝叶子的可能性更大,因此只有部分结果可以代替拒绝叶子,造成偏见。如果您在设置一定数量的迭代后停止拒绝,就会产生类似的二叉树和类似的偏差。 (但是,根据应用的不同,这种偏见可以忽略不计。随机整数生成还存在安全方面的问题,这些问题太复杂了,无法在此答案中讨论。)

快速骰子滚子实现

在前面给出的意义上,有很多 optimized 算法的示例。其中之一是J. Lumbroso(2013)的Fast Dice Roller(在下面实现),也许其他示例是2004年的answer to a similar Stack Overflow questionMath Forum的算法。另一方面,所有算法surveyed by M. O'Neill都不是最优的,因为它们一次依赖于生成随机位的块。另请参阅我在integer generating algorithms上的笔记。

以下是Fast Dice Roller的JavaScript实现。请注意,它使用拒绝事件和循环来确保它没有偏见。 nextBit()是一个随机位生成器(例如Math.random()<0.5 ? 1 : 0,就JavaScript中最终依赖的随机位而言,它不一定有效)。

function randomInt(minInclusive, maxExclusive) {
 var maxInclusive = (maxExclusive - minInclusive) - 1
 var x = 1
 var y = 0
 while(true) {
    x = x * 2
    var randomBit = nextBit()
    y = y * 2 + randomBit
    if(x > maxInclusive) {
      if (y <= maxInclusive) { return y + minInclusive }
      // Rejection
      x = x - maxInclusive - 1
      y = y - maxInclusive - 1
    }
 }
}

减少钻头浪费

回想一下,“最优”整数生成器(例如上面的Fast Dice Roller)平均平均至少使用log2(n)位(下限),或者平均使用此下限的2位。可以使用多种技术来使算法(甚至不是最佳算法)更接近此理论下限,包括批处理和随机性提取。在以下内容中进行了讨论:

以下是“批处理”的示例:要生成从0到9的四个随机数字,只需在[0,9999]中生成一个随机整数,然后将结果数字分解为数字。相反,生成八个随机数字将涉及间隔[0,99999999]。