< 返回版块

bnyu 发表于 2023-11-13 18:01

一般来说浮点数因为在不同操作系统,不同CPU架构上,可能导致不同的细微差别,最终导致相同的输入在不同架构系统上无法得到相同的输出。所以通常需要确定性计算的程序会采用定点数而避免使用浮点数计算。

但是因为性能原因(根据我自己的用例测试,定点数模拟比浮点数慢2倍左右)以及库大多都是基于浮点数的,所以我准备切换回使用浮点数,但依然必须保证确定性(误差我并不关心)

我打算使用 nalgebra 这个线性代数库,而不是自己再造轮子。因为我注意到 nalgebra 库有一个 libm-force 的 feature 描述写的是:For forcing the use of the libm crate in order to make sure the implementations of all special math functions will be the same of all the platforms. 似乎是能保证跨平台确定性的?

同时我继续搜索发现 glam 库也有libm这个feature,同时它还有个fast-math:By default, glam attempts to provide bit-for-bit identical results on all platforms. Using this feature will enable platform specific optimizations that may not be identical to other platforms. 似乎只要不开这个且开了libm就可以保证跨平台可复现的完全一致的结果。

所以我有几个问题:

  1. 导致浮点数无法跨平台确定性的根本原因是什么? 根据我搜索的资料,绝大部分语言都是遵行 IEEE 754 标准的,IEEE 754 不仅规定了浮点数的表示规范,而且还规定了浮点数基本计算以及舍入规范。大部分现代 CPU(FPU)也都是遵循IEEE 754 标准的。那么为什么还会有这个问题,是因为不同系统的libc不同,里面的math函数实现不同导致? 那么只要不使用系统自带的libc,而是用libm代替,就像nalgebra,glam里允许做的那样,就可以避免这个问题?
    但这似乎不是唯一的原因,编译器对不同平台进行的优化也要触发浮点不确定性,有些用了SSE,有些用了x87?那么glam等库又是怎么规避这个问题的呢?SIMD会对确定性有影响吗(例如glam里的core-simd有无影响,我看 nalgebra 也大量使用了SIMD指令,且不是可选开启的,不知是否会有影响)?

  2. Rust在什么情况下可以保证浮点数的确定性? 这个我主要找到这个资料 float semantics RFC 里面内容有一句:只要我不使用类似from_bits这种直接构造浮点NaN等 then your code will behave perfectly deterministically and according to the IEEE specification 因为目前Rust已经满足IEEE 754-2008规范了。好像我只要小心避免NaN输入,基本就已经是满足确定性的了? Pre-RFC

  3. 我是否可以使用 nalgebra 同时得到跨平台的确定性,同时需要注意些什么? 如果可以保证确定性,那么我就开始切换到这上面了。(我手里只有两台设备,虽然还都是x86_64的,还没写代码验证但我想先理论上确定是否可行) 同时库之外的浮点运算,基本的加减乘除应该是确定的吧,只要不unsafe的构造NaN,不使用标准库里的三角函数(这些应该依赖于libc),还有什么需要注意的吗?

评论区

写评论
SlimeYummy 2024-01-30 17:55

尝试了一点点跨平台确定性的工作,其实就是这库 https://github.com/SlimeYummy/ozz-animation-rs 。

原理上讲影响跨平台确定性的因素基本是你说的:编译器、指令集、数学库。

编译器,你不混编C代码的话,Rust默认配置就行。

指令集,要么不用SIMD,用的话只推荐nightly的Portable SIMD,SSE和NEON有很多有效位数的坑,比如知名的_mm_rsqrt_ps,NEON等价指令与SSE有效位数不同。好在Portable SIMD里提供的指令可以放心用。不过我仍然遇到过x86_64与aarch64上某些计算产生的NaN符号位不同的问题,会导致f & 0x80000000这种取符号的操作产生不同结果。

数学库,只能以实测为准,比如我使用glam就遇到了默认features无法保证确定性的问题(https://github.com/bitshifter/glam-rs/issues/468)。

所以,最大的感受还是,一定要在目标平台上测试!!

作者 bnyu 2023-11-15 10:01

嗯,主要想确认是:相同的代码,会不会在编译优化下,使得在有些架构上执行了simd,或者不同方式的simd,有些又没有,导致相同的浮点操作代码,却导致了在不同平台架构上出现不一致结果。 如果会怎么可以避免 那个RFC提到过unsafe或者等效unsafe封装的函数可能导致相同代码出现不确定结果。

--
👇
Bai-Jinlin: 你这个问题提起了我的兴趣刚才研究了下。

运算的不确定性确实可能出现在一些simd的操作上。

例如一条指令同时计算a乘b加c,这个指令与分两步计算的结果在一些特殊值的组合上有一些内部舍入的误差。

我不确定这种问题是因cpu而异还是这种舍入有固定的标准。

use core::arch::x86_64::*;
fn main() {
    unsafe {
        let a = 0.112811446;
        let b = 0.6541757;
        let c = 0.794073;
        println!("{}", a * b + c);

        let a = _mm_set1_ps(a);
        let b = _mm_set1_ps(b);
        let c = _mm_set1_ps(c);

        let d = _mm_add_ps(_mm_mul_ps(a, b), c);
        println!("{:?}", d);

        let e = _mm_fmadd_ps(a, b, c);
        println!("{:?}", e);
    }
}
Bai-Jinlin 2023-11-14 12:18

你这个问题提起了我的兴趣刚才研究了下。

运算的不确定性确实可能出现在一些simd的操作上。

例如一条指令同时计算a乘b加c,这个指令与分两步计算的结果在一些特殊值的组合上有一些内部舍入的误差。

我不确定这种问题是因cpu而异还是这种舍入有固定的标准。

use core::arch::x86_64::*;
fn main() {
    unsafe {
        let a = 0.112811446;
        let b = 0.6541757;
        let c = 0.794073;
        println!("{}", a * b + c);

        let a = _mm_set1_ps(a);
        let b = _mm_set1_ps(b);
        let c = _mm_set1_ps(c);

        let d = _mm_add_ps(_mm_mul_ps(a, b), c);
        println!("{:?}", d);

        let e = _mm_fmadd_ps(a, b, c);
        println!("{:?}", e);
    }
}
1 共 3 条评论, 1 页