假设我有一个真正随机的字节数组(例如从熵源捕获).
byte[] myTrulyRandomBytes = MyEntropyHardwareEngine.GetBytes(8);
Run Code Online (Sandbox Code Playgroud)
现在,我想获得一个随机的双精度浮点值,但值介于0和正1之间(就像Random.NextDouble()函数执行一样).
简单地将8个随机字节的数组传递给BitConverter.ToDouble()可能产生奇怪的结果,但最重要的是,结果几乎不会小于1.
我对位操作很好,但浮点数的格式对我来说一直都很神秘.我尝试了许多位组合来应用随机性,并且总是最终发现数字要么刚刚超过1,要么非常接近0,要么非常大.
有人可以解释哪些位应该在a double中随机,以使其在0和1范围内随机?
这比你想象的容易; 它的全部是关于缩放(从0-1范围到其他范围时也是如此).
基本上,如果你知道你有64个真正的随机位(8个字节),那么就这样做:
double zeroToOneDouble = (double)(BitConverter.ToUInt64(bytes) / (decimal)ulong.MaxValue);
Run Code Online (Sandbox Code Playgroud)
当你的"随机"位实际上不是一般的随机时,就会出现这种算法的问题.那时你需要一个专门的算法,比如Mersenne Twister.
虽然给出了工作答案,但我会给另一个,看起来更糟但不是:
long asLong = BitConverter.ToInt64(myTrulyRandomBytes, 0);
double number = (double)(asLong & long.MaxValue) / long.MaxValue;
Run Code Online (Sandbox Code Playgroud)
从一个ulongto 转换的问题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]
Run Code Online (Sandbox Code Playgroud)
鉴于我的建议,它将更好地编译:
vxorps xmm0,xmm0,xmm0
vcvtsi2sd xmm0,xmm0,rcx
vdivsd xmm0,xmm0,mmword ptr [00000060h]
Run Code Online (Sandbox Code Playgroud)
两者都使用.NET 4中的x64 JIT进行了测试,但这一般适用于将ulonga 转换为a 的好方法double.
不要担心丢失熵的位数:首先在0.0和1.0之间只有2 62个双打,并且大多数较小的双打不能被选择,因此可能的结果数量甚至更少.
请注意,这个以及所呈现的ulong示例可以精确地导致1.0并且在相邻结果之间分配具有稍微不同的间隙的值,因为它们不除以2的幂.您可以将它们排除在1.0之外,并获得稍微更均匀的间距(但请参见下面的第一个图,有一堆不同的间隙,但这种方式非常规则),如下所示:
long asLong = BitConverter.ToInt64(myTrulyRandomBytes, 0);
double number = (double)(asLong & long.MaxValue) / ((double)long.MaxValue + 1);
Run Code Online (Sandbox Code Playgroud)
作为一个非常好的奖金,你现在可以将除法改为乘法(两个幂通常有倒数)
long asLong = BitConverter.ToInt64(myTrulyRandomBytes, 0);
double number = (double)(asLong & long.MaxValue) * 1.08420217248550443400745280086994171142578125E-19;
Run Code Online (Sandbox Code Playgroud)
对于ulong,如果你真的想要使用它,也一样.
既然你似乎也对如何使用double-bits技巧感兴趣,我也可以证明这一点.
由于整个有效数/指数交易,它不能真正以超直接的方式完成(只是重新解释位和那就是它),主要是因为选择指数一致地解决了麻烦(使用统一指数,数字必然是成团的因为大多数指数都存在,所以优先接近0.
但是如果指数是固定的,那么很容易在该区double域内制造一个统一的指数.这不能是0比1,因为它跨越了很多指数,但它可以是1到2然后我们可以减去1.
因此,首先屏蔽不属于有效数字的位:
x &= (1L << 52) - 1;
Run Code Online (Sandbox Code Playgroud)
放入指数(1.0 - 2.0范围,不包括2)
x |= 0x3ff0000000000000;
Run Code Online (Sandbox Code Playgroud)
重新解释并调整偏移量1:
return BitConverter.Int64BitsToDouble(x) - 1;
Run Code Online (Sandbox Code Playgroud)
应该也很快.一个副作用是,这一次它真的不费一点熵的,因为只有52,但有可能是53.这样,总是会留下最少的显著位零(隐式比特抢断位).
对分发有一些担忧,我现在将讨论.
选择随机(u)长并将其除以最大值的方法显然具有统一选择的(u)长,并且在此之后发生的事实际上是有趣的.结果可以合理地称为均匀分布,但是如果你把它看作一个离散的分布(它实际上是它),它看起来(定性地)像这样:(所有的minifloats例子)
忽略"更粗"的线条和更宽的间隙,这只是直方图很有趣.这些图使用2的幂除法,因此实际上没有间距问题,它只是奇怪地绘制.
Top是使用太多位时发生的情况,就像将完整(u)长除以其最大值时所发生的那样.这使得较低的浮点数具有更好的分辨率,但是许多不同的(u)长度被映射到较高区域中的相同浮点数.这不一定是坏事,如果你"缩小",密度在各处都是一样的.
底部是当分辨率限制在最坏情况(0.5到1.0区域)时所发生的情况,您可以通过首先限制位数然后执行"缩放整数"处理来完成.我的第二个建议是没有实现这一点,它仅限于分辨率的一半.
对于它的价值,NextDouble在System.Random非负int的范围内进入0.0 .. 1.0范围.这个决议显然要低得多.它也使用了一个int不能int.MaxValue并且因此缩放大约1 /(2 31 -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);
}
Run Code Online (Sandbox Code Playgroud)