在C#中从0到1之间的随机字节数组获取随机双精度(浮点)值?

8
假设我有一个字节数组,它们是真正的随机数(例如从熵源捕获而来)。
byte[] myTrulyRandomBytes = MyEntropyHardwareEngine.GetBytes(8);

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

仅仅将8个随机字节传递到BitConverter.ToDouble()中可能会产生奇怪的结果,但最重要的是,结果几乎永远不会小于1。

我擅长位操作,但是浮点数的格式总是让我感到神秘。我尝试了许多比特位的组合来应用随机性,但总是发现数字要么略微超过1,要么总是非常接近0或非常大。

有人能解释一下应该在double中使哪些位随机,以使其在0和1之间随机吗?

6个回答

9

虽然已经给出了可行的答案,我会提供另一个答案,看起来更糟糕但实际上并不是:

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 个 double,大多数比较小的 double 不能被选择,因此可能的结果数量更少。

请注意,这以及所提供的 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-bit巧妙地处理它,我也可以展示一下。
  • 由于整个尾数/指数的关系,它不能以超级直接的方式完成(只需重新解释位并完成),主要原因是选择均匀的指数会带来麻烦(具有均匀指数,数字必定偏向于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个。这种方法总是使最低有效位为零(隐式位占用了一位)。
对于分布存在一些顾虑,我现在来解决。
选择一个随机的长整型数并将其除以最大值的方法显然具有均匀选择的长整型数,之后发生的事情其实很有趣。结果可以被称为均匀分布,但如果你把它看作离散分布(实际上它就是),它看起来(定性地)像这样:(所有示例都是关于minifloats)

float distribution

忽略“粗”线和更宽的间隙,那只是直方图有点搞笑。这些曲线使用了除以2的幂,所以在现实中没有间隔问题,只是绘制得有点奇怪。
上面是当你使用过多位时会发生的情况,例如在将完整的长整型数除以其最大值时发生的情况。这将为较低的浮点数提供更好的分辨率,但许多不同的长整型数会被映射到高区域中的相同浮点数上。如果你“缩小”密度,这不一定是坏事。
底部是当分辨率在任何情况下都被限制为最差情况(0.5到1.0区域)时会发生的情况,你可以通过先限制位数然后执行“比例整数”操作来实现这一点。我第二个建议使用的位操作无法达到这个目标,它只能达到该分辨率的一半。
值得一提的是,System.Random中的NextDouble将非负int缩放到0.0..1.0范围内。它的分辨率明显比可能的更低。它还使用了一个不能为int.MaxValueint,因此缩放大约为1/(2 31-1)(不能用double表示,所以有些舍入),因此实际上存在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);
}

4

这比你想象中的要容易得多,关键在于扩展(当从0-1范围扩展到其他范围时也是如此)。

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

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

这类算法的问题在于当你的“随机”位并非完全随机时会出现问题。这时候你需要使用特殊的算法,如梅森旋转算法

1
转换为 decimal,而不是 doubleDouble 没有足够的精度来区分高 ulong 值之间的差异。例如 (double)(ulong.MaxValue) == (double)(ulong.MaxValue - 1) - Jakub Lortz
在这种情况下,我们仅针对某些值范围失去精度,因此分布不再是均匀的。 - Jakub Lortz
使用这种方法可能会导致意外的 NaN 值。 - Dai

3

我不知道这是否是最佳解决方案,但它应该能胜任:

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

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


我理解这种方法可能会导致意外的 NaN 值。 - Dai
@Dai为什么?ulong不能比double更大。 - MetaColon
我了解这与IEEE-754值的二进制表示有关:我知道有些事情需要做(我不熟悉),以避免设置错误的位。请记住,IEEE-754值是结构化类型(在位级别上),而不是像“int”这样的简单标量。 - Dai

2
如果您关注随机数的质量,对于目前出现的答案要非常怀疑。直接使用Int64BitsToDouble的答案一定会出现NaN和无穷大的问题。例如,0x7ff0000000000001是一个完全好的随机比特位模式,但转换成了NaN(还有成千上万的其他值也是)。尝试将其转换为ulong并进行缩放,或在确保满足各种比特模式限制后将其转换为double的那些答案不会有NaN问题,但它们很可能会存在分布问题。可表示的浮点数在(0,1)上不是均匀分布的,因此任何随机选择所有可表示值之间的方案都不会产生所需的均匀性值。为了安全起见,只需使用ToInt32并将该int用作Random的种子。(为了更加安全,拒绝0.) 这不会像其他方案那样快,但它会更加安全。已经付出了大量的研究和努力,使RNG以一种不容易看出的方式变得更好。

2
为确保 long 值在 0 到 1 的范围内,您可以应用以下掩码:
long longValue = BitConverter.ToInt64(myTrulyRandomBytes, 0);
longValue &= 0x3fefffffffffffff;

结果值保证在范围[0, 1)内。
备注。 值0x3fefffffffffffff非常接近于1,并将显示为1,但实际上比1稍微小一些。

如果您想使生成的值更大,则可以将指数的更多位设置为1。例如:

longValue |= 0x03c00000000000000;

总结一下:在dotnetfiddle上的示例

1

一个简单的代码片段,可以帮助你打印出位(bit)。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接