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)
显然我没有做正确的事情,因为最后几个小数是根据需要“随机”的。为什么我不能正确地将它们向上移动?
如果有人指出我的错误,我将不胜感激。
该序列看起来就像将小整数填充到指数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 的指数字段,使用标量常量进行所有数学运算,甚至不会诱使编译器在运行时执行此操作。