【问题标题】:RNGCryptoServiceProvider - generate number in a range faster and retain distribution?RNGCryptoServiceProvider - 更快地生成范围内的数字并保留分布?
【发布时间】:2011-09-12 01:18:33
【问题描述】:

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

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

当范围足够宽以致有相当大的机会获得结果时,这很好,但今天早些时候我遇到了一个范围足够小(在 10,000 个数字内)以至于可能需要一段时间的情况。

所以我一直在尝试寻找一种更好的方法来实现体面的分布,但速度更快。但现在我正在研究我在学校根本没有做过的更深层次的数学和统计,或者至少我已经忘记了!

我的想法是:

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

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

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

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

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

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

【问题讨论】:

  • while(result &lt; min &amp;&amp; result &gt; max) { } 我想这两个条件操作数之间应该有|| 运算符而不是&amp;&amp;
  • 是的 - 刚刚编辑。真不敢相信这不是早点发现的。

标签: c# security random


【解决方案1】:
public int RandomNumber(int min = 1, int max = int.MaxValue)
{
    using (var rng = new RNGCryptoServiceProvider())
    {
       byte[] buffer = new byte[4];
       rng.GetBytes(buffer);
       return (int)(BitConverter.ToUInt32(buffer, 0) >> 1) % ((max - min) + 1);
    }
}

【讨论】:

  • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
【解决方案2】:

让我谈谈在平均使用的随机位数方面“最佳”的随机整数生成算法。在这篇文章的其余部分,我们将假设我们有一个“真正的”随机生成器,它可以产生无偏且独立的随​​机位。 (这里,一个随机“字节”将是一个由 8 个随机位组成的块。)

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

然而,任何最优整数生成器也是无偏的,一般来说,在最坏的情况下会永远运行,正如 Knuth 和 Yao 所展示的那样。回到二叉树,n 个结果标签中的每一个都留在二叉树中,因此 [0, n) 中的每个整数都可以以 1/n 的概率出现。但是如果 1/n 有一个非终止的二元展开式(如果 n 不是 2 的幂就是这种情况),这棵二叉树必然要么——

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

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

因此,一般而言,随机整数生成器可以是任一无偏恒定时间(甚至两者都不是),但不能两者兼而有之。 二叉树的概念表明,一般来说,在不引入偏差的情况下,无法“修复”运行时间不确定的最坏情况。例如,模约简(例如,rand() % n)相当于一棵二叉树,其中拒绝叶被标记的结果替换——但由于可能的结果比拒绝叶多,因此只有一些结果可以代替拒绝离开,引入偏见。如果您在一定次数的迭代后停止拒绝,则会产生类似的二叉树 - 以及类似的偏差。 (但是,根据应用程序,这种偏差可能可以忽略不计。随机整数生成也有安全方面的问题,在这个答案中讨论太复杂了。)

快速掷骰子实现

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

以下是快速掷骰子的 JavaScript 实现。请注意,它使用拒绝事件和循环来确保它是公正的。 nextBit() 是一种产生独立无偏随机位的方法(例如,Math.random()&lt;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
    }
 }
}

减少钻头浪费

回想一下,“最佳”整数生成器,例如上面的快速骰子滚轮,平均使用至少 log2(n) 位(下限),或者平均在该下限的 2 位范围内。有多种技术可用于使算法(甚至不是最佳算法)更接近这个理论下限,包括批处理和随机性提取。这些讨论在:

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

    【讨论】:

    • 这在我的测试中完美运行(改编),所以我想知道为什么这不是正确的答案?为什么其他解决方案会努力让 while 循环进行多次迭代?如果我听到对此的一些回应,我会对这个解决方案感觉更好。底线是:这个带模数的解决方案正是在其他地方生成随机字符串的方式(@codesinchaos,见这里:stackoverflow.com/a/19068116/264031),每次都需要对一个设定大小的数组进行索引(因此真的:a最小-最大)。那么为什么这还不够呢?
    • 由于@MarkusOlsson 链接到的文章现在已经死了,您必须使用时间机器:web.archive.org/web/20090304194122/http://msdn.microsoft.com:80/… 这应该可以解释为什么您在此处给出的实现需要while 循环。重要的不仅仅是我们得到快速的结果,而且每个整数都有相同的出现机会。如果你用模数压缩结果,你会立即在概率分布中造成不平衡。
    • @AndrasZoltan 代替文章打开的页面有一个指向文章的可下载 .chm 版本的链接。事实上,他们可能几乎每篇文章都有一个链接。它按年/月组织。只需向下滚动即可查看。 download.microsoft.com/download/3/A/7/…
    【解决方案4】:

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

    请注意,这些测量仅描述了一个非常具体的非现实世界场景,并且只能用于指导,测量您的场景以获得正确的结果。

    【讨论】:

    • 真的很棒,比我的尝试好多了!我不敢相信 Google-fu 没有提出那篇文章,但更重要的是,线程安全版本也会有很大帮助。我会下载并试一试,但根据这些配置文件,我已经可以看到它是我一直在寻找的。一个非常慷慨的答案,谢谢:)
    • 使用它可以将查找随机数所花费的时间减少至少 5 倍。仍然具有均匀分布。我是一个快乐的人:)
    • @Andreas:太棒了!真的很高兴我能提供帮助,虽然大部分的赞美应该指向 MSDN 文章背后的两位策划者 :) 我已经在生产中使用我的实现已经有一段时间了,没有遇到任何错误,但如果你发现了一些你就让我知道。
    • 请注意,max 参数在这里有一个奇怪的名称,因为 min 是包容性的,max 是独占性的。因此,对于掷骰子,您必须询问范围 [1, 7) - 即 max = 7 而不是 max = 6
    • 可以在此处找到 MSDN 文章:MSDNMagazineSeptember2007en-us.chm,采用 CHM 格式。 (列,.Net 事项:来自 CryptoRandom 的故事)
    【解决方案5】:

    以下是@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);
            }
    

    【讨论】:

    • 既然@MarkusOlsson 链接到的文章现在已经死了,你必须使用时间机器:web.archive.org/web/20090304194122/http://msdn.microsoft.com:80/… 这应该解释了为什么在你在这里给出的实现中需要while 循环。重要的不仅仅是我们得到快速的结果,而且每个整数都有相同的出现机会。如果你用模数压缩结果,你会立即在概率分布中造成不平衡。
    【解决方案6】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-01-08
      • 1970-01-01
      • 1970-01-01
      • 2013-05-02
      • 1970-01-01
      • 2019-10-25
      • 2019-07-03
      相关资源
      最近更新 更多