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

Converting u64 to f64 between 0..1

提问人:BlueOyster 提问时间:11/17/2023 最后编辑:BlueOyster 更新时间:11/18/2023 访问量:103

问:

我需要一个非常快速的伪随机数生成器来处理我一直在做的项目。到目前为止,我已经实现了 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);

输出:

[1.0, 1.0000000074505808, 1.0000000149011616, 1.0000000223517425, 1.0000000298023235, 1.0000000372529039, ...]

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

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

Rust 随机 浮点 SIMD

评论

1赞 Tim Roberts 11/17/2023
你为什么要这样做?您生成的值看起来与浮点数完全不同。如果你的范围是 64 位,那么只需将整数除以 2^63。from_bits
0赞 BlueOyster 11/17/2023
@Tim Roberts:那是我第一次直觉和尝试。它几乎只给了我 10^-12 范围内的数字。我想我可以乘以一个常数来获得我想要的范围,但所有这些操作都太昂贵了。我曾经尝试从现有的板条箱中获取实现;这就是他们使用的。from_bits
0赞 BlueOyster 11/17/2023
@PeterCordes 你是绝对正确的。我只是通过打入一些大型 u64 来快速测试一组更大的“真实”种子。我得到了我想要的行为。
0赞 BlueOyster 11/17/2023
@PeterCordes 关于只随机化尾数的想法实际上非常好;我非常喜欢。
0赞 BlueOyster 11/17/2023
@PeterCordes 您能详细说明一下为什么在这里使用 SIMD 向量效率低下吗?对于我的应用程序,我需要一次生成 10 到 1000 个“随机”数字(总是 2 的幂)。我认为将其分解为 SIMD 向量会更有效。另外,是的,对不起 0。我在那里有它,因为我正在尝试添加不同的指数值;我忘了在发布前删除它。

答:

9赞 Peter Cordes 11/17/2023 #1

这个序列看起来就像你用 1.0 的指数将小整数塞进位模式的尾数中得到的,所以你得到的是 1.0 加上微小的数量。没有那么小;https://www.binaryconvert.com/result_double.html?decimal=049046048048048048048048048048055052053048053056048056 显示该数字由位模式表示,该位模式在尾数中仅设置了 2 位。f640, 1, 2, 3, ...f640x3FF0000002000001

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

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

如果您确实有统一的随机值(例如,使用真实种子,或让生成器对非零种子运行多次迭代),则使用指数 from 进行 ORing 也会使指数随机化,从而导致巨大的值。但大小总是大于 1.0,除了具有全 1 指数的 NaN(如果尾数为零,则为无穷大)。也是一个随机标志。OR无法清除位,并且由于指数偏差,IEEE浮点幅度随着整数位模式的增加而单调增加。https://en.wikipedia.org/wiki/Double-precision_floating-point_formatu641.0

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

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

Chux 的另一种方法(保值转换,如 C 强制转换)然后除法(实际上是乘以逆法)无法在没有 AVX-512 的情况下在 x86 上有效地完成从 到 的打包转换。如何使用 SSE/AVX 高效地执行 double/int64 转换?- 它需要多个指令,而不是替换指数和减法。(使用 AVX-512,替换指数字段也变得更加高效,只需使用覆盖指数+符号字段的位掩码即可。u64f64vpternlogd


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

我假设那里有不同的指数场,但为什么要向量加法?只是会给你 1.0 的指数字段,使用标量常量进行所有数学运算,甚至不会诱使编译器在运行时这样做。u64x64::splat(1023) + u64x64::splat(0)u64x64::splat((1023 + offset) << 52)

评论

0赞 BlueOyster 11/17/2023
由于我的声望不超过 15 分,我(不幸的是)无法对这个答案投赞成票。但是,请知道,这对于解决我的问题和提供更深入的理解非常有帮助。
1赞 Severin Pappadeux 11/19/2023
只是为了添加答案的链接 corsix.org/content/higher-quality-random-floats