关于c#:RNGCryptoServiceProvider – 更快地生成范围内的数字并保留分布?

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

首先我在打电话,请原谅格式不好!

我已经做了很多搜索,没有找到确切的答案。如果没有一个,很公平,但我相信比我聪明的人一定有一个很好的答案!

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

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

当范围足够宽,有很好的机会得到结果时,这是很好的,但是今天早些时候我遇到了一个场景,范围足够小(在10000个数字内),需要一个年龄。

所以我一直在想一个更好的方法,可以实现一个体面的分配,但会更快。但现在我开始深入研究我在学校根本没学过的数学和统计学,或者至少如果我学过的话,我已经把它全忘了!

我的想法是:

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

正如我所说,这可能是非常幼稚的,但我相信它将以比当前实现更快的速度返回一个狭窄范围内的匹配。我现在不在电脑前,所以不能测试,明天早上英国时间就要做。

当然,速度不是我唯一关心的问题,否则我只会随机使用(如果有人足够友善,需要在那里打几个勾号来正确格式化-他们不在Android键盘上!).

我对上述方法最大的担心是,我总是丢弃由prng生成的最多7位,这看起来很糟糕。我想办法把它们加入(例如简单的添加)但是它们看起来非常不科学!

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

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


史蒂芬·图布和肖恩·法卡斯合作写了一篇关于msdn的优秀文章,名为《密码随机》中的故事,如果你正在试验RNGCryptoServiceProviders,你一定要读。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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));
        }
    }
}

本文对实现的选择进行了推理,并详细分析了随机性的损失,以及它们为产生高质量随机数所采取的步骤。

线程安全缓冲密码随机

我编写了一个斯蒂芬类的扩展实现,它使用了一个随机缓冲区,以最小化调用getBytes()的开销。我的实现还使用同步来提供线程安全性,从而可以在所有线程之间共享实例以充分利用缓冲区。

我为一个非常具体的场景编写了这个脚本,因此您当然应该根据应用程序的特定争用和并发属性来分析是否对您有意义。如果你不想查看的话,我把代码扔到了Github上。

基于stephen toub和shawn farkas实现的线程安全缓冲密码随机

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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                              |
|---------------------|------------------------------------|

请注意,这些度量只是描述一个非常具体的非真实世界场景,应该只用于指导、测量场景以获得正确的结果。


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

虽然您可能会丢弃未使用的字节,但我会对它进行一次尝试,因为从这个方法和modulo方法(您不使用的方法)中获得了非常好的速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

你能做的另一件事是比较你在哪里思考位。但是,我会关注范围是否适合字节、短、整型或长。然后,您可以用该类型的max对int结果进行模运算(给出低阶位)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//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循环,这将很慢,并且基于未知的迭代次数。

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

But, if we squeeze results with modulo, we immediately create an imbalance in the probability distributions.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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范围内的任意数字。

一个小小的考验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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


以下是对@andrey wd上述答案的改编,但不同之处在于,您只需发送一个已经生成的随机数(在本例中,可以将ulong更改为uint)。当您需要一个范围内的多个随机数时,这是非常有效的,您可以通过RNGCryptoServiceProvider生成一个这样的数数组(或者其他任何方法,即使使用Random,如果适合您的需要)。当需要在一个范围内生成多个随机数时,我确信这会有更高的性能。你所需要的只是储存一些随机数字来输入函数。请看上面我在@andrey wd's答案上的注释,我很好奇为什么其他人不做这种不需要多次迭代的简单模路。如果真的有一个多迭代路线的必要原因,我会很高兴听到它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    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。我还假设通过将字节奇异转换为数组类型来提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
    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;
    }

用途:

1
2
3
4
5
6
        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);
        }