在 0..1 之间将 u64 转换为 f64

Blu*_*ter 2 random floating-point simd rust

我需要一个非常快速的伪随机数生成器来用于我一直在进行的项目。到目前为止,我已经实现了 xorshift 算法,并且可以生成伪随机 u64。但是,我需要将这些 u64 转换为 0 到 1 范围内的浮点值。

我主要使用这个这个作为参考。

由于某种原因,我无法接近我想要的行为;这让我感到困惑,因为我使用了与此处完全相同的方法。尽管我看到实现没有任何差异,但我得到了不同的结果。

    let seeds: [u64; 64] = core::array::from_fn(|i| i as u64);

    let bitshift12 = u64x64::splat(12);
    let bitshift25 = u64x64::splat(25);
    let bitshift27 = u64x64::splat(27);
    
    let bitshift52 = u64x64::splat(52);
    
    let mut random_states = Simd::from(seeds);
    
    random_states ^= random_states >> bitshift12;
    random_states ^= random_states << bitshift25;
    random_states ^= random_states >> bitshift27;
    
    random_states = random_states | ((u64x64::splat(1023) + u64x64::splat(0)) << bitshift52);
    
    let mut generated = Simd::<f64, 64>::from_bits(random_states);
    
    println!("{:?}", generated);
Run Code Online (Sandbox Code Playgroud)

输出:

[1.0, 1.0000000074505808, 1.0000000149011616, 1.0000000223517425, 1.0000000298023235, 1.0000000372529039, ...]
Run Code Online (Sandbox Code Playgroud)

显然我没有做正确的事情,因为最后几个小数是根据需要“随机”的。为什么我不能正确地将它们向上移动?

如果有人指出我的错误,我将不胜感激。

Pet*_*des 9

该序列看起来就像将小整数填充到指数f64为 1.0 的位模式的尾数中所得到的,因此你得到 1.0 加上少量的值。不那么小0, 1, 2, 3, ...; https://www.binaryconvert.com/result_double.html?decimal=04904604804804804804804804804805505205 ​​3048053056048056显示数字由尾数中仅设置了2位的f64位模式表示。0x3FF0000002000001

不过,这看起来像是在 xoshiro 迭代之后从 seed = 1 开始得到的位模式。请注意,第一个移位是向右移动,移出唯一留下 0 的位。下一步是向左移动,导致两个设置位。然后最后右移 27 将它们都移出,再次与 0 进行异或,使它们保持不变。

seeds[i] = i因此,在 xoshiro 的一步之后,你的极其非随机的种子就会导致这些非随机的尾数。(并且seeds[0]永远不会变成非零;xoshiro 需要非零种子,因为移位和异或永远无法从零创建非零位。)

如果您确实有统一的随机u64值(例如,使用真实的种子,或者让生成器对非零种子运行多次迭代),则将它们与指数进行“或”运算也1.0会使指数随机化,从而产生巨大的值。但其数量级始终大于 1.0,除了具有全 1 指数的 NaN(如果尾数为零,则为无穷大)。也是随机的标志。OR 无法清除位,并且由于指数偏差,IEEE 浮点数大小随着整数位模式的增加而单调增加。 https://en.wikipedia.org/wiki/Double- precision_floating-point_format

如果您屏蔽随机数u64以仅保留低 52 位,这样您只需随机化尾数,您就可以轻松地在 中获得统一的随机数[1.0, 2.0)。正如 Chux 所说,在您链接的问答中(如何将大量伪随机位转换为 0 到 1 之间的统计随机浮点值?),1.0从中减去是获取数字的标准方法[0.0, 1.0)

越接近 0.0(指数越小),在减去两个附近的数字后,尾数的尾随零就越多:指数越小,可表示的值越接近,但我们想要均匀分布。此方法只有 52 位熵。这可能没问题,但理论上您可以检查指数字段并使用可变计数移位 + OR 来随机化低尾数位。

Chux 的其他方法(保值转换,如 C 类型转换)然后除法(实际上是乘以逆)在 x86 上无法高效完成,无需 AVX-512 进行从u64到 的打包转换f64如何使用SSE/AVX高效执行double/int64转换?- 它需要多个指令,比替换指数和减法还要多。(使用 AVX-512,替换指数字段也变得更加高效,只需一个vpternlogd带有覆盖指数+符号字段的位掩码的单个字段。)


顺便说一句,除非编译器优化回标量立即数,否则使用移位计数的 SIMD 向量看起来效率不高u64x64 bitshift12。至少在 x86 和 AArch64 上,向量移位可以使用标量计数,因此我希望能够random_states >> 12编译为vpsrlq ymm, ymm, 12(使用 AVX2),而不需要 AVX2 变量计数移位和向量常量来进行计数,例如vpsrlvq ymm, ymm, ymm. (Zen 2 上的每 2 个周期吞吐量与立即计数移位的每周期 1 个吞吐量:https://uops.info/。但在 Zen 3 及更高版本以及 Intel Skylake 及更高版本上,吞吐量是相同的。但是如果编译器实际上必须从 64x 数组加载计数向量u64,这很糟糕)。

我猜想u64x64::splat(1023) + u64x64::splat(0)是为了玩不同的指数域,但为什么要向量相加呢?只是u64x64::splat((1023 + offset) << 52)会给你 1.0 的指数字段,使用标量常量进行所有数学运算,甚至不会诱使编译器在运行时执行此操作。