虽然已经给出了可行的答案,但我会给出另一个,看起来更糟,但事实并非如此:
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);
}