【问题标题】:Get random double (floating point) value from random byte array between 0 and 1 in C#?从 C# 中 0 到 1 之间的随机字节数组中获取随机双精度(浮点)值?
【发布时间】:2017-04-21 23:01:30
【问题描述】:

假设我有一个真正随机的字节数组(例如从熵源捕获)。

byte[] myTrulyRandomBytes = MyEntropyHardwareEngine.GetBytes(8);

现在,我想获得一个随机的双精度浮点值,但介于 0 和正 1 之间(就像 Random.NextDouble() 函数执行的那样)。

简单地将 8 个随机字节的数组传递给 BitConverter.ToDouble() 会产生奇怪的结果,但最重要的是,结果几乎永远不会小于 1。

我对位操作很好,但浮点数的格式对我来说一直很神秘。我尝试了许多位组合来应用随机性,结果总是发现数字要么刚刚超过 1,要么总是非常接近 0,要么非常大。

有人能解释一下double 中的哪些位应该随机化以使其在 0 和 1 范围内随机化吗?

【问题讨论】:

    标签: c# random


    【解决方案1】:

    虽然已经给出了可行的答案,但我会给出另一个,看起来更糟,但事实并非如此:

    long asLong = BitConverter.ToInt64(myTrulyRandomBytes, 0);
    double number = (double)(asLong & long.MaxValue) / long.MaxValue;
    

    ulong 转换为double 的问题是硬件不直接支持它,所以它编译成这样:

     vxorps      xmm0,xmm0,xmm0 
     vcvtsi2sd   xmm0,xmm0,rcx   ; interpret ulong as long and convert it to double
     test        rcx,rcx         ; add fixup if it was "negative"
     jge         000000000000001D 
     vaddsd      xmm0,xmm0,mmword ptr [00000060h] 
     vdivsd      xmm0,xmm0,mmword ptr [00000068h] 
    

    而根据我的建议,它会编译得更好:

     vxorps      xmm0,xmm0,xmm0 
     vcvtsi2sd   xmm0,xmm0,rcx 
     vdivsd      xmm0,xmm0,mmword ptr [00000060h] 
    

    两者都在 .NET 4 中使用 x64 JIT 进行了测试,但这通常适用,只是没有一个很好的方法可以将 ulong 转换为 double

    不用担心丢失一点熵:在 0.0 和 1.0 之间只有 262 个双精度数,并且大多数较小的双精度数无法选择,因此可能的结果就更少了。

    请注意,此示例以及所提供的 ulong 示例可能会导致精确的 1.0 并在相邻结果之间分配具有略微不同间隙的值,因为它们不会除以 2 的幂。您可以将它们更改为排除 1.0 并获得更均匀的间距(但请参见下面的第一张图,有很多不同的间隙,但这种方式非常规则),如下所示:

    long asLong = BitConverter.ToInt64(myTrulyRandomBytes, 0);
    double number = (double)(asLong & long.MaxValue) / ((double)long.MaxValue + 1);
    

    作为一个非常好的奖励,您现在可以将除法更改为乘法(2 的幂通常有倒数)

    long asLong = BitConverter.ToInt64(myTrulyRandomBytes, 0);
    double number = (double)(asLong & long.MaxValue) * 1.08420217248550443400745280086994171142578125E-19;
    

    如果你真的想使用 ulong,同样的想法。


    由于您似乎也对如何使用 double-bits 诡计特别感兴趣,所以我也可以展示一下。

    由于整个有效数/指数交易,它不能真正以超级直接的方式完成(只需重新解释位就可以了),主要是因为选择统一的指数会带来麻烦(使用统一的指数,数字因为大多数指数都在那里,所以必然会优先聚集在 0 附近)。

    但如果指数是固定的,则很容易制作一个在该区域内统一的double。这不可能是 0 到 1,因为它跨越了很多指数,但它可以是 1 到 2,然后我们可以减去 1。

    所以首先屏蔽掉不属于有效位的位:

    x &= (1L << 52) - 1;
    

    代入指数(1.0 - 2.0范围,不包括2)

    x |= 0x3ff0000000000000;
    

    重新解释和调整 1 的偏移量:

    return BitConverter.Int64BitsToDouble(x) - 1;
    

    应该也很快。一个不幸的副作用是,这一次它确实确实花费了一点熵,因为只有 52 个但可能有 53 个。这种方式总是使最低有效位为零(隐式位窃取有点)。


    有一些关于分发的问题,我现在将解决这些问题。

    选择随机 (u)long 并将其除以最大值的方法显然具有统一选择的 (u)long,然后发生的事情实际上很有趣。结果可以合理地称为均匀分布,但如果您将其视为离散分布(实际上是),它(定性地)看起来像这样:(所有 minifloats 的示例)

    忽略“较粗”的线条和较宽的间隙,这只是直方图很有趣。这些图都是用2的幂除,所以实际不存在间距问题,只是画的很奇怪。

    Top 是当您使用太多位时会发生的情况,例如将完整的 (u)long 除以其最大值时发生的情况。这为较低的浮点数提供了更好的分辨率,但许多不同的 (u)long 被映射到较高区域的相同浮点数上。这不一定是坏事,如果你“缩小”,密度在任何地方都是一样的。

    当分辨率在任何地方都被限制在最坏情况(0.5 到 1.0 区域)时会发生什么情况,您可以通过首先限制位数然后进行“缩放整数”处理来做到这一点。我的第二个建议是使用 bit hacks 并没有实现这一点,它仅限于该分辨率的 一半

    对于它的价值,System.Random 中的 NextDouble 将非负的 int 缩放到 0.0 .. 1.0 范围。其分辨率显然比它可能的低很多。它还使用了一个int,它不能是int.MaxValue,因此按比例缩放大约 1/(231-1)(不能用双精度表示,所以稍微四舍五入),所以实际上有相邻可能结果之间有 33 个略有不同的间隙,但大多数间隙的距离相同。

    由于int.MaxValue 与如今的暴力破解相比很小,您可以轻松生成NextDouble 的所有可能结果并检查它们,例如我运行了这个:

    const double scale = 4.6566128752458E-10;
    double prev = 0;
    Dictionary<long, int> hist = new Dictionary<long, int>();
    for (int i = 0; i < int.MaxValue; i++)
    {
        long bits = BitConverter.DoubleToInt64Bits(i * scale - prev);
        if (!hist.ContainsKey(bits))
            hist[bits] = 1;
        else
            hist[bits]++;
        prev = i * scale;
        if ((i & 0xFFFFFF) == 0)
            Console.WriteLine("{0:0.00}%", 100.0 * i / int.MaxValue);
    }
    

    【讨论】:

      【解决方案2】:

      这比你想象的要容易;都是关于缩放的(从 0-1 范围到其他范围时也是如此)。

      基本上,如果您知道您有 64 个真正随机位(8 个字节),那么只需执行以下操作:

      double zeroToOneDouble = (double)(BitConverter.ToUInt64(bytes) / (decimal)ulong.MaxValue);
      

      当您的“随机”位实际上不是均匀随机时,这种算法的问题就来了。这时候你就需要一个专门的算法,比如Mersenne Twister

      【讨论】:

      • 转换为decimal,而不是doubleDouble 没有足够的精度来区分高 ulong 值。例如(double)(ulong.MaxValue) == (double)(ulong.MaxValue - 1)
      • 在这种情况下,我们只对某些范围的值失去精度,因此分布不再均匀。
      • 使用这种方法可能会导致无意的NaN 值。
      【解决方案3】:

      我不知道这是否是最好的解决方案,但它应该可以完成这项工作:

      ulong asLong = BitConverter.ToUInt64(myTrulyRandomBytes, 0);
      double number = (double)asLong / ulong.MaxValue;
      

      我所做的只是将字节数组转换为ulong,然后除以它的最大值,这样结果就在 0 和 1 之间。

      【讨论】:

      • 我知道这种方法可能会导致无意的NaN 值。
      • @Dai 为什么? ulongs 不能大于 doubles。
      • 我认为这与 IEEE-754 值的二进制表示有关:我知道您需要做一些事情(我不熟悉)以避免设置错误的位。请记住,IEEE-754 值是结构化类型(按位级别),而不是像 int 这样的简单标量。
      【解决方案4】:

      要确保long 的值在 0 到 1 的范围内,您可以应用以下掩码:

      long longValue = BitConverter.ToInt64(myTrulyRandomBytes, 0);
      longValue &= 0x3fefffffffffffff;
      

      结果值保证在[0, 1)范围内。
      备注。 0x3fefffffffffffff 的值非常非常接近 1,将打印为 1,但它确实比 1 小了一点。

      如果要使生成的值更大,可以将指数的更高位设置为1。例如:

      longValue |= 0x03c00000000000000;
      

      总结:example on dotnetfiddle.

      【讨论】:

        【解决方案5】:

        如果您关心生成的随机数的质量,请对目前出现的答案非常怀疑。

        那些直接使用 Int64BitsToDouble 的答案肯定会遇到 NaN 和无穷大的问题。例如,0x7ff0000000000001,一个非常好的随机位模式,转换为 NaN(成千上万的其他位模式也是如此)。

        那些尝试转换为 ulong 然后缩放,或者在确保满足各种位模式约束后转换为 double 的,不会出现 NaN 问题,但它们很可能会出现分布问题。可表示的浮点数并非均匀分布在 (0, 1) 上,因此任何在所有可表示值中随机选取的方案都不会产生具有所需一致性的值。

        为了安全起见,只需使用 ToInt32 并将该 int 用作 Random 的种子。 (为了更加安全,拒绝 0。)这不会像其他方案那样快,但会更安全。大量的研究和努力已经投入到使 RNG 变得更好的方面,而这些方面并不是很明显。

        【讨论】:

          【解决方案6】:

          一段简单的代码可以为你打印出来。

          for (double i = 0; i < 1.0; i+=0.05)
          {
              var doubleToInt64Bits = BitConverter.DoubleToInt64Bits(i);
              Console.WriteLine("{0}:\t{1}", i, Convert.ToString(doubleToInt64Bits, 2));
          }
          
          0.05:   11111110101001100110011001100110011001100110011001100110011010
          0.1:    11111110111001100110011001100110011001100110011001100110011010
          0.15:   11111111000011001100110011001100110011001100110011001100110100
          0.2:    11111111001001100110011001100110011001100110011001100110011010
          0.25:   11111111010000000000000000000000000000000000000000000000000000
          0.3:    11111111010011001100110011001100110011001100110011001100110011
          0.35:   11111111010110011001100110011001100110011001100110011001100110
          0.4:    11111111011001100110011001100110011001100110011001100110011001
          0.45:   11111111011100110011001100110011001100110011001100110011001100
          0.5:    11111111011111111111111111111111111111111111111111111111111111
          0.55:   11111111100001100110011001100110011001100110011001100110011001
          0.6:    11111111100011001100110011001100110011001100110011001100110011
          0.65:   11111111100100110011001100110011001100110011001100110011001101
          0.7:    11111111100110011001100110011001100110011001100110011001100111
          0.75:   11111111101000000000000000000000000000000000000000000000000001
          0.8:    11111111101001100110011001100110011001100110011001100110011011
          0.85:   11111111101011001100110011001100110011001100110011001100110101
          0.9:    11111111101100110011001100110011001100110011001100110011001111
          0.95:   11111111101110011001100110011001100110011001100110011001101001
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-12-17
            • 1970-01-01
            • 2014-09-22
            • 2011-07-07
            • 2021-06-05
            • 2021-01-06
            相关资源
            最近更新 更多