是否可以在Rust中编写Quake的快速InvSqrt()函数?


101

这只是为了满足我自己的好奇心。

是否有此实现:

float InvSqrt (float x)
{
   float xhalf = 0.5f*x;
   int i = *(int*)&x;
   i = 0x5f3759df - (i>>1);
   x = *(float*)&i;
   x = x*(1.5f - xhalf*x*x);
   return x;
}

在Rust中?如果存在,则发布代码。

我尝试过但失败了。我不知道如何使用整数格式编码浮点数。这是我的尝试:

fn main() {
    println!("Hello, world!");
    println!("sqrt1: {}, ",sqrt2(100f64));
}

fn sqrt1(x: f64) -> f64 {
    x.sqrt()
}

fn sqrt2(x: f64) -> f64 {
    let mut x = x;
    let xhalf = 0.5*x;
    let mut i = x as i64;
    println!("sqrt1: {}, ", i);

    i = 0x5f375a86 as i64 - (i>>1);

    x = i as f64;
    x = x*(1.5f64 - xhalf*x*x);
    1.0/x
}

参考:
1 . Quake3快速InvSqrt()的起源-第1页
2. 了解Quake快速逆平方根
3. FAST INVERSE SQUARE ROOT.pdf
4. 源代码:q_math.c#L552-L572


C#版本:是否可以
Flyq

4
据我了解,由于违反了严格的别名规则,因此该代码在C中是UB 。执行此类类型修饰的标准方法是使用union
trentcl

4
@trentcl:我也不认为union有效。memcpy确实有效,尽管它很冗长。
Matthieu M.19年

14
@MatthieuM。用union进行类型修剪是完全有效的C,但不是有效的C ++。
Moira

4
从纯好奇心的角度来看,我认为这个问题很好,但是请您了解时代已经改变。在x86上,1999年奔腾III引入的rsqrtssrsqrtps指令比该代码更快,更准确。ARM NEON与vrsqrte此类似。无论如何,Quake III对此进行的任何计算都可能会在这几天在GPU上完成。
benrg

Answers:


87

我不知道如何使用整数格式编码浮点数。

有一个函数:f32::to_bits返回u32。还有另一个方向的函数:f32::from_bits它以u32as作为参数。这些功能优于mem::transmute后者,因为后者unsafe难以使用。

这样,这里是实现InvSqrt

fn inv_sqrt(x: f32) -> f32 {
    let i = x.to_bits();
    let i = 0x5f3759df - (i >> 1);
    let y = f32::from_bits(i);

    y * (1.5 - 0.5 * x * y * y)
}

游乐场


此函数在x86-64上编译为以下程序集:

.LCPI0_0:
        .long   3204448256        ; f32 -0.5
.LCPI0_1:
        .long   1069547520        ; f32  1.5
example::inv_sqrt:
        movd    eax, xmm0
        shr     eax                   ; i << 1
        mov     ecx, 1597463007       ; 0x5f3759df
        sub     ecx, eax              ; 0x5f3759df - ...
        movd    xmm1, ecx
        mulss   xmm0, dword ptr [rip + .LCPI0_0]    ; x *= 0.5
        mulss   xmm0, xmm1                          ; x *= y
        mulss   xmm0, xmm1                          ; x *= y
        addss   xmm0, dword ptr [rip + .LCPI0_1]    ; x += 1.5
        mulss   xmm0, xmm1                          ; x *= y
        ret

我还没有找到任何参考程序集(如果有的话,请告诉我!),但是对我来说似乎还不错。我只是不确定为什么将浮点数移入eax以进行移位和整数减法。上交所寄存器可能不支持这些操作吗?

clang 9.0可以-O3将C代码编译为基本相同的程序集。这是一个好兆头。


值得指出的是,如果您确实想在实践中使用它:请不要。正如benrg 在评论中指出的那样,现代x86 CPU为此功能提供了专门的指令,该指令比该hack更快,更准确。不幸的是,1.0 / x.sqrt() 似乎并未对该指令进行优化。所以,如果你真的需要的速度,使用_mm_rsqrt_ps内部函数可能是要走的路。但是,这再次需要unsafe代码。在这个答案中,我将不做详细介绍,因为少数程序员实际上会需要它。


4
根据《英特尔技术指南》,没有整数移位运算只能将128位寄存器模拟的最低32位移位为addssmulss。但是,如果可以忽略xmm0的其他96位,则可以使用该psrld指令。整数减法也是如此。
fsasm

我承认几乎不了解铁锈,但“不安全”基本上不是fast_inv_sqrt的核心属性吗?完全不尊重数据类型等。
Gloweye19年

12
@Gloweye这是我们谈论的另一种“不安全”。快速逼近与最佳点相距太远而产生的不良价值,与快速和松散且行为不确定的事物相比。
Deduplicator19年

8
@Gloweye:从数学上讲,其最后一部分fast_inv_sqrt只是一个Newton-Raphson迭代步骤,以找到的更好近似值inv_sqrt。这部分没有什么不安全的。窍门在第一部分,找到了一个很好的近似值。之所以sqrt(pow(0.5,x))=pow(0.5,x/2)
可行

1
@fsasm:是的;movd到EAX并返回是当前编译器错过的优化。(是的,调用约定传递/返回标float在XMM低元素,并允许高位是垃圾,但需要注意的是,如果它。零扩展,它可以很容易地保持这样的:右移位不引入非零元素,也不减去_mm_set_epi32(0,0,0,0x5f3759df),即movd加载。您需要movdqa xmm1,xmm0先复制reg psrld。旁路延迟从FP指令转发到整数的mulss等待时间,反之亦然,这被等待时间隐藏了
Peter Cordes

37

union在Rust中鲜为人知的是实现了这一点:

union FI {
    f: f32,
    i: i32,
}

fn inv_sqrt(x: f32) -> f32 {
    let mut u = FI { f: x };
    unsafe {
        u.i = 0x5f3759df - (u.i >> 1);
        u.f * (1.5 - 0.5 * x * u.f * u.f)
    }
}

是否criterion在x86-64 Linux盒子上使用板条箱进行了一些微基准测试。令人惊讶的是,Rust自己sqrt().recip()的速度最快。但是,当然,任何微基准测试结果都应一粒盐。

inv sqrt with transmute time:   [1.6605 ns 1.6638 ns 1.6679 ns]
inv sqrt with union     time:   [1.6543 ns 1.6583 ns 1.6633 ns]
inv sqrt with to and from bits
                        time:   [1.7659 ns 1.7677 ns 1.7697 ns]
inv sqrt with powf      time:   [7.1037 ns 7.1125 ns 7.1223 ns]
inv sqrt with sqrt then recip
                        time:   [1.5466 ns 1.5488 ns 1.5513 ns]

22
我毫不惊讶sqrt().inv()是最快的。现在,sqrt和inv都是单一指令,并且运行很快。厄运是在无法完全假设存在硬件浮点的时代编写的,而像sqrt这样的先验功能肯定是软件。基准为+1。
马丁·邦纳

4
令我惊讶的是,transmute显然是不同的,从to_from_bits-我期望那些被指令相当于甚至优化前。
trentcl

2
@MartinBonner(也没关系,但sqrt不是先验功能。)
benrg

4
@MartinBonner:任何支持除法的硬件FPU通常也会支持sqrt。需要IEEE“基本”运算(+-* / sqrt)才能产生正确的舍入结果;这就是为什么SSE提供所有这些操作,但不提供exp,sin或其他任何操作的原因。实际上,除法和sqrt通常在相同的执行单元上运行,设计方式类似。请参阅HW div / sqrt单位详细信息。无论如何,相乘还是比较快,特别是在延迟方面。
彼得·科德斯

1
无论如何,Skylake的div / sqrt流水线要比以前的架构好得多。有关Agner Fog表中的部分摘录,请参见浮点除法与浮点乘法。如果您没有在一个循环中做很多其他工作,那么sqrt + div是一个瓶颈,您可能想要使用HW快速互惠sqrt(而不是地震hack)+牛顿迭代。尤其是对于FMA而言,即使没有延迟,也可以提高吞吐量。 快速矢量化rsqrt和SSE / AVX的倒数取决于精度
Peter Cordes

10

您可以std::mem::transmute用来进行所需的转换:

fn inv_sqrt(x: f32) -> f32 {
    let xhalf = 0.5f32 * x;
    let mut i: i32 = unsafe { std::mem::transmute(x) };
    i = 0x5f3759df - (i >> 1);
    let mut res: f32 = unsafe { std::mem::transmute(i) };
    res = res * (1.5f32 - xhalf * res * res);
    res
}

您可以在此处找到一个实时示例:这里


4
不安全没有错,但是有一种方法可以在没有显式不安全块的情况下执行此操作,因此我建议使用f32::to_bits和重写此答案f32::from_bits。它也明显不同于转换,而大多数人可能将其视为“魔术”。
Sahsahae

5
@Sahsahae我刚刚使用您提到的两个功能发布了答案:)我同意,unsafe在这里应避免使用它,因为这是没有必要的。
Lukas Kalbertodt
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.