文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载导读本文系统解读 Rust RFC 3514Feature Name:float_semantics——这份提案为 Rust 的浮点运算f32、f64及未来新增的f16、f128等类型给出了正式的行为规范回答了一直困扰 Rust 社区的这一串浮点运算允许产生这个结果吗这一类问题。读完本文你将掌握Rust 浮点运算在哪些场景严格遵循 IEEE 754-2008、NaN 位模式在何种条件下是非确定性的、const上下文中的浮点运算与运行时行为为何可以不同以及内联汇编与浮点环境之间存在的未定义行为边界。这份规范来自仓库中的 text/3514-float-semantics.mdrustc 的当前实现已经与该规范一致因此 RFC 通过后编译器本身无需改动。背景为什么 Rust 需要一份浮点语义规范Rust 社区积累了大量关于浮点运算的开问题核心都归结为一句话这一串浮点运算是否被允许产生给定的结果 RFC 3514 的动机部分明确指出造成这种困惑的根源来自三个方向LLVM 优化带来的出乎意料的效应MIR 与 LLVM 优化中的 bug某些目标平台上的 bug或至少是不符合规范的行为。这些问题在 unsafe-code-guidelines 的相关讨论中得到了集中收集最终催生了这份 RFC。它的目的很直接不再让用户对什么被保证、什么不被保证蒙在鼓里。值得一提的是RFC 3514 的制定与 Rust 生态中其他浮点相关 RFC 形成了呼应。例如 text/3453-f16-and-f128.md 提案新增 IEEE 754 binary16f16与 binary128f128类型而 text/3173-float-next-up-down.md 为f32/f64增加了 IEEE 754 标准定义的next_up/next_down方法。RFC 3514 恰好为这些类型的运算行为提供了统一的语义底座——规范中明确浮点类型指f32和f64以及未来可能加入的f16、f128等类似类型。规范核心一句话概括的保证RFC 的 Guide-level 部分给出了一个对绝大多数 Rust 程序员都成立的简明保证浮点类型上的原始运算通常产生与 IEEE 754-2008 精确匹配的结果。具体地说只要你的代码满足以下条件就观察不到任何非确定性行为完全符合 IEEE 规范不对 NaN 使用to_bits、copysign、is_sign_negative、is_sign_positive不用from_bits构造 NaN不使用与这些安全方法等价的任何不安全操作。一旦你确实在 NaN 上使用了上述操作那么你看到的确切行为就可能随编译器版本、编译器标志、目标架构而变化甚至是非确定性的——即对相同输入执行两次相同运算可能得到不同结果。在这些情况下结果并不总是符合 IEEE 规范。此外还有一条稳定保证当浮点值只是被传递pass around时其内容包括 NaN 的位不会改变。这意味着 NaN 位模式一旦产生在变量间复制、传参、存取的整个生命周期内保持稳定不会在传递过程中被悄悄改写。参考级规范哪些运算、遵循哪些规则被规范覆盖的运算集合RFC 明确列出了受该规范约束的运算、-一元和二元、*、/、%、abs、copysign、mul_add、sqrt、涉及浮点类型的as类型转换以及浮点类型上的所有比较运算。除下述 NaN 例外情况外这些运算产生与 IEEE 754-2008精确匹配的结果具体约束为舍入模式采用roundTiesToEven向偶数舍入唯一的例外是浮点到整数的转换其舍入向零round towards zero默认异常处理、无陷阱no traps没有 abruptUnderflow/flush-to-zero即不启用下溢冲刷为零%运算的行为与 C 语言的fmod一致该运算不在 IEEE 规范之内。浮点环境的状态位不可观察RFC 明确了两条重要立场Rust 认为浮点状态位floating-point status bits不可观察Rust不支持以替代舍入模式执行浮点运算也不支持修改浮点控制位floating-point control bits。这两条是后续浮点环境假设章节中 UB 判定的直接前提。NaN 语义本规范中最核心也最微妙的部分术语约定Rust 将 NaN 值的位结构划分为三部分符号位sign bit静默/信号位quiet/signaling bit为1表示静默 NaNQNaN为0表示信号 NaNSNaN下文简称为静默位载荷payload尾数mantissa即 significand中除静默位之外的其余部分。一个浮点 NaN 值 符号位 静默位 载荷。NaN 运算的四种合法输出选项当上述运算的输出是 NaN、且该运算不是位运算一元-、abs、copysign时规范规定结果的符号位非确定而静默位与载荷从以下四类选项中非确定地选取Preferred NaN首选 NaN静默位被置位载荷全为零。Quieting NaN propagation静默化 NaN 传播静默位被置位载荷复制自任意一个输入为 NaN 的操作数。若输入输出载荷大小不一致发生在as类型转换时输出比输入小则丢弃低位比特输出比输入大则在低位补零。Unchanged NaN propagation原样 NaN 传播静默位与载荷均复制自任意一个输入为 NaN 的操作数。大小不一致时规则同上但有一个陷阱若输出小于输入丢弃低位后载荷可能变为 0而全零的尾数编码的是无穷大infinity不可能出现在信号 NaN 上因此某些输入下原样 NaN 传播无法发生。Target-specific NaN目标特定 NaN静默位被置位载荷从目标特定的额外 NaN 载荷集合中选取。该集合可依赖输入操作数的取值。在x86、ARM、RISC-V32 位与 64 位上该集合为空但在其他架构上可能非空——集合非空的目标应在合适的位置例如平台支持页予以文档化。例如在 wasm 上若任一输入 NaN 不具有首选的全零载荷、或任一输入 NaN 是 SNaN则该集合包含所有可能的载荷否则为空在 SPARC 上该集合只包含全一all-one载荷。由这些规则导出的保证基于上述四种选项RFC 给出了两个对编程实践非常有用的推论若所有输入 NaN 都是静默的或根本没有输入 NaN则输出 NaN 一定是静默的。信号 NaN 只可能作为输入值出现例如fmul SNaN, 1.0可能被简化成 SNaN 而非 QNaN。若所有输入 NaN 都是首选的或没有输入 NaN且目标没有额外 NaN 载荷则输出 NaN 保证是首选的。换句话说SNaN 永远不会凭空出现——程序里看到信号 NaN 的唯一途径是用from_bits或等价的不安全操作亲手创建它。这与Rust 浮点运算可以产生信号 NaN但仅当输入就是信号 NaN 时的总述完全一致也是该 RFC 直接照搬 LLVM 新 NaN 规则的原因详见先例章节。非确定性的粒度执行时选择一次一个容易误解的点是非确定性到底发生在何时RFC 的表述是——非确定性的选择发生在运算被执行的那一刻。也就是说NaN 产生运算的结果是一个稳定的位模式多次查看这些位会得到一致结果但用相同输入执行两次相同运算可能产生不同的结果。例如0.0 / 0.0在硬件上两次运行可能产生符号位或载荷不同的 NaN。const上下文中的浮点语义确定性底线与不保证编译期求值CTFE即const项及其他 CTFE 入口的求值为了类型系统的健全性soundness必须是确定性的。RFC 在此只给出最低限度的保证当浮点运算在const中产生 NaN 时结果位模式是输入的某个确定的函数且满足运行时浮点语义的所有约束但这个函数本身未被规定允许随目标、Rust 版本甚至编译器标志而变化。特别地没有任何保证说明编译期求值的选择与运行时的选择一致。也就是说下面的断言是允许失败的实际上在当前的 Rust 版本上它确实失败use std::hint::black_box; const C: f32 0.0 / 0.0; fn main() { let c: f32 0.0 / black_box(0.0); assert_eq!(C.to_bits(), c.to_bits()); }更具体地看在运行时同一个const fn用相同参数调用两次结果本身就可能是不同的release 模式下因优化差异而确实会发生。因此下面的代码所体现的两层含义需要区分清楚const fn div(x: f32) - i32 { unsafe { std::mem::transmute(x / x) } } // 这一断言不保证总是成功。这是新特性目前所有 // 你能写出的 const fn 都保证这一点。 assert_eq!(div(0.0), div(0.0)); // 因此编译期与运行期可以观察到不同的结果。 const C: i32 div(0.0); assert_eq!(C, div(0.0));第一个断言div(0.0) div(0.0)在实践中极不可能失败因为它要求两次div调用被不同地优化第二个断言C div(0.0)在当前 nightly 的 debug 模式下确实会失败。甚至同一个表达式如0.0 / 0.0在求值同一个const项时出现两次、或出现在两个不同的const项中都可能产生不同结果。类型系统唯一需要的保证是在不同的编译单元中重复求值some_crate::SOME_CONST会得到一致结果——所以规范只保证这一点。对const fn的解锁意义这份const语义设计解决了阻塞浮点运算进入const fn的最后一个开放问题。RFC 明确预期在 RFC 被接受后const_fn_floating_point_arithmetic特性门feature gate与const fn to_bits方法即可稳定。这与 text/0911-const-fn.md 所确立的const fn演进方向一脉相承——后者奠定了const fn作为编译期可计算函数的基本机制而 RFC 3514 则为在const fn中安全地使用浮点运算补齐了语义拼图。关于浮点环境的假设内联汇编与 UB 边界RFC 明确指出上述保证对用户代码而言是一个黑盒——如何实现是细节。但当 Rust 与内联汇编混用时这些实现细节就变得可观察了内联汇编不得以任何方式改变浮点运算的行为离开内联汇编块时浮点控制位必须与进入时完全一致否则构成 UB。这属于一般性原则的特例——内联汇编违反 Rust 编译器在目标硬件上实现 Rust 语义所依赖的任何不变量即为 UB。观察浮点异常状态得到的结果完全未规定unspecifiedRust 浮点运算可能并不在源码中原本书写的位置执行而且即使源码中没有任何浮点运算浮点状态位也可能发生变化。RFC 将这一情形类比为未开启#pragma STD FENV_ACCESS的 C 语言。一个著名的应用陷阱trap-on-NaN 调试程序员常利用浮点控制位做调试开启 NaN 生成时的陷阱trap当浮点运算生成 NaN 时终止程序或触发调试器。在上述措辞下这是 UB。原因在于编译器转换会改变陷阱触发的时间与位置优化器可能把a / b移出循环而没有证明循环一定会执行如果该运算恰好产生 NaN程序就会触发陷阱——尽管按照 Rust 源码语义原本根本没有浮点运算被执行。这就是编译器依赖浮点运算永不触发陷阱的含义也是违反该假设构成 UB 的意义。根据 RFC 作者的评估目前这种 UB 最坏的后果也就如此陷阱可能在原程序本无浮点运算时触发也可能在确实生成了 NaN 时因优化移动或删除了运算不触发。将 trap-on-NaN 作为调试手段的程序员仍可使用该技术但必须知晓这些注意事项——且这不是稳定保证未来措辞如何稳定化尚无定论。平台差异与目标特定问题RFC 承认某些目标因平台能力或 ABI 的深层次原因无法正确实现该规范且不太可能完全修复。这些属于 Rust 在这些目标上的实现 bug应在平台支持页面上逐一文档化并跟踪目标问题影响32 位 x86有/无 SSE浮点返回值经 x87 寄存器传递会改变 NaN 载荷函数返回前后查看浮点位模式可能得到不同结果违反NaN 载荷一旦产生即为稳定位模式的保证32 位 x86 无 SSE2i586 目标x87 寄存器被更广泛使用位模式不稳定的机会更多运算内部使用不同精度计算可能连 NaN 之外的常规结果都偏离 IEEE 754-2008更彻底的规范失配旧版 MIPSsignaling与quiet的解释与上述规范正好相反任何 NaN 产生运算都可能非确定地产生任意信号或静默NaNLLVM 当前无法为这一目标实现自己的 NaN 语义规范在该目标上实际退化32 位 ARMNEON SIMD 运算总是 flush-to-zero若 LLVM 为该目标自动向量化将偏离 IEEE 语义。目前尚不清楚是否实际触发但若未来向 Rust 用户暴露 NEON 运算将与其非 SIMD 对应物应有的 NaN 规则冲突潜在的非一致隐患在 text/3983-bf16.md 的未决问题一节中这一思路被推广到了新的f16bbfloat16类型上当目标 ABI 无法明确定义某种浮点类型的调用约定、且问题无法修复时RFC 3514 提出的在支持页记录兼容性方案被引为合理折中。权衡与替代方案为什么这样设计运行时语义的替代选项替代方案一制定一个对所有目标或至少所有 tier 1 目标都成立的规范。这至少要求让 i686 目标合规即承认浮点值从函数返回时 NaN 位模式允许改变——这显然不是合理的语义也不应强加给不受 x87 影响的平台。tier 1 的 i686 目标在-C target-cpupentium下同样有无 SSE2问题若要让其当前形态合规则几乎无法对f64运算行为做出任何声明。相反最广泛使用的目标64 位 x86、64 位 ARM、以及日益流行的 RISC-V已完全合规。让所有目标完全合规的希望渺茫因此文档化不合规被视为合理妥协总好过为了照顾老目标而让主要新目标上的体验变差。这并不妨碍代码特意为浮点行为有 bug 的目标做移植适配但确立了代码一般不应假设非 IEEE 合规结果舍入差异、flush-to-zero或不稳定 NaN 位的总体预期。替代方案二完全确定性的规范。为什么不直接规定 Rust 中的 NaN 与底层硬件完全一致两个原因(1) 这样的保证会阻止对 IEEE 浮点而言正确的甚至最基本的变换例如运算的交换律、加法/乘法的单位元——a * b在双方都是 NaN 时产生的 NaN 与b * a并不至少并非在所有硬件上相同(2) LLVM 在架构上目前无法在其常量折叠器中反映目标相关行为且 LLVM 开发者没有表现出提供匹配目标行为保证的兴趣除非通过不被优化的严格浮点内联函数。此外有些情况下底层目标行为本身就是非确定性的——例如 wasm。替代方案三运算永不产生信号 NaN。返回信号 NaN 违反 IEEE浮点运算永不产生信号 NaN的基本保证。但 LLVM 认为将x * 1.0折叠为x是合法的因此若x是信号 NaN乘法输出就可能是信号 NaN同理(x: f32) as f64 as f32可能被折叠成x连类型转换都能产生信号 NaN。鉴于 Rust 中异常状态标志不可观察信号 NaN 基本无用武之地没有理由为了信号 NaN 的保证而约束优化。替代方案四对静默/信号位完全不提供任何保证。这能简化规范并让旧 MIPS 硬件合规。但 C 语言中有一个操作——pow——恰恰会对静默 NaN 输入产生非 NaN 结果例如pow(1, exponent)对任意exponent即使是 NaN都返回1而实践中pow(1, sNaN)返回 NaN。也就是说pow确实区分静默与信号 NaN。若允许任意运算随意产生信号 NaN就无法再依赖pow的这个性质。正因为静默/信号之分有意义规范才提供保证程序几乎只会遇到静默 NaN获得信号 NaN 的唯一安全途径是from_bits。替代方案五不做严格语义保证。严格 IEEE 754-2008 保证会阻碍许多变换如把a*b c转成 FMA因为舍入不同会改变结果。提供较弱保证虽然也能让高精度的 x87 指令合规但无法约束这类变换对程序行为的影响范围它们经由to_bits甚至纯浮点运算产生可观察的不同结果并可能滚雪球般地变成任意行为差异而且不存在精度单调性——FMA 替换乘加虽然让该次运算结果精度更高却可能让后续计算如x*x - x*x被变换为x.mul_add(x, -x*x)后产生非零结果精度更低。因此更可取的做法是为这类改变语义的变换提供显式 opt-in例如快速浮点运算内联函数不提供严格 IEEE 语义以及基于这些内联函数的专用类型。const语义的替代选项替代方案一const必须确定且与运行时一致。本 RFC 首次接受const fn可在运行时非确定地行为并在编译期产生目标/版本/标志相关结果编译期与运行期调用还可能不同。其理由禁止此类行为的收益是投机性的不安全代码可以利用const fn的确定性但没有任何已知的实际例子会这么做——而且这会不必要地把函数限制在编译期可达的范围内而强制确定性的代价很大——鉴于浮点运算的非确定性规范在获得确定性规范之前无法让浮点运算进入const fn。总结就是以现有技术水平实现确定性浮点行为太难接受非确定性的代价很低不值得为此阻塞const fn中的浮点运算。另外需要强调本 RFC 并不提供让代码可靠地检测当前运行在编译期还是运行期的途径。替代方案二const在将产生 NaN 时直接失败。即每次 NaN 产生时直接报错。但这是破坏性变更const C: f32 0.0 / 0.0;自 Rust 1.0 起就在 stable 上正常工作。即便采用此方案const fn仍会执行运行时非确定的运算不安全代码依然不能把const fn当作运行时确定的标记——它继承了前一方案的所有缺点只是避免了 NaN 选择在const中可观察。其核心优势是避免const结果因编译器更新时未指定的编译期 NaN 改变而变化但这种依赖本就罕见且已有更常见的先例repr(Rust)类型的布局经size_of、offset_of观察同样依赖未指定的实现细节也会随编译器更新而变。RFC 还提出可以考虑增加一个 lint 来警告 CTFE 中意外产生 NaN 的情况。替代方案三const符号化跟踪 NaN 值其位在编译期被观察时才失败。即像跟踪指针那样符号化地跟踪 NaN把 NaN 载荷位的选择推迟到 codegen若 const 求值期间观察 NaN 的位如调用to_bits则中止。这能让const C 0.0/0.0;继续工作但要求const fn is_nan成为内建函数intrinsic并需要编译期解释器投入可与符号化指针支持相提并论的大量工作。RFC 作者认为未指定的 NaN 位模式在 const 求值中被观察的缺点很小此方案回报低侵入性更小的做法仍是 lint 反对在const求值中产生 NaN。先例C23、LLVM、Java 与 wasm 的立场C23澄清了对信号 NaN 的立场——未规定信号 NaN 行为之处信号 NaN 的行为是实现定义的要么当作 IEC 60559 静默 NaN要么当作 IEC 60559 信号 NaN。C23 对 NaN 内部的位模式只字未提因此严格来说任何 NaN boxing 都必须在每次运算后重新规范化 NaN——但实践中没人这么做代码依赖的是编译器实现更严格的语义然而 GCC、MSVC 都没有文档化它们确实提供了更严格的语义。编译器对该标准的解读似乎是若运算的输入是信号 NaN则输出可以是信号 NaN。LLVM近期采纳了新的 NaN 规则本 RFC逐字照搬这些规则。这意味着即使算术运算能产生信号 NaN也有保证信号 NaN 不会凭空出现。LLVM 并未实际文档化它使用 IEEE 浮点语义但在几乎所有目标上事实如此例外即上文所述使用 x87 指令的目标。Java要求严格的 IEEE 754-2008 合规为在无 SSE 的 32 位 x86 上实现这一点投入了大量工作但他们似乎没有处理 NaN 载荷位的规范化问题尽管这些位可以通过floatToRawIntBits观察到。wasm保证若所有输入 NaN 都是规范的canonical则任何输出 NaN 都是规范的。这里的canonical定义为载荷最高位为 1 而其余位为 0的 NaN与本 RFC 所称的preferred一致。RFC 特意回避 wasm 术语因为canonical在 IEEE 规范中已有不同含义同理本 RFC 认为 NaN 载荷不包括静默/信号位而 wasm 将静默/信号位视为载荷的一部分。wasm 对符号位不做规定因此存在两个规范 NaN在标准的静默/信号位解读下两者都是静默的。Rust 内部围绕浮点语义的讨论已持续多年本 RFC 是这些讨论unsafe-code-guidelines 中收集了大量相关内容的集大成者。未决问题与未来可能性尚待解决的问题LLVM 并没有实际文档化它使用 IEEE 浮点语义它自己的优化 pass 却默认如此是否应为此担忧是否还有其他存在浮点麻烦的目标所有剩余目标的额外 NaN 集合究竟包含什么该规范在多大程度上适用于平台内联函数platform intrinsics一方面期望平台内联函数具有平台指令的行为另一方面部分平台内联函数是用可移植的 LLVMsimd内联函数实现的受上述 NaN 非确定性约束——因此目前至少部分平台内联函数的事实语义与平台实际行为并不匹配。未来的方向目前规范只覆盖标量f32/f64不稳定版本的std::simdSIMD 对应物理应遵循相同规则主要障碍是 32 位 ARM NEON 总是 flush-to-zero——可将其文档化为该目标的 errata或为std::simd避免使用 NEON。未来可尝试为0.0 / 0.0产生的符号位以及一般的无 NaN 输入却产生 NaN的运算争取确定性规范但 x86 与 ARM 行为不同x86 产生负 NaNARM 产生正 NaNLLVM 总是将其常量折叠为正 NaN——此举受限于让 LLVM 浮点常量折叠器更懂目标。用非默认浮点环境运行 Rust 代码有潜在价值但存在重大开放问题若不假设浮点环境处于默认状态浮点运算的编译期折叠将变得困难甚至不可能。任何允许替代浮点运算的提案都必须解释如何避免拖累只想用默认设置代码的优化。C 语言通过#pragma STD FENV_ACCESS与#pragma STDC FENV_ROUND处理一旦 clang 支持这些指令Rust 应评估是否满意其语义并考虑在 Rust 中同样暴露。为支持 fast-math 变换未来可引入独立的快速路径内联函数/类型。也许存在一种规范方式使运算返回信号 NaN 时符号位是确定的——但需要先提出合适的规范再说服 LLVM 采纳。可以增加一个 lint当编译期浮点运算产生 NaN 时触发警告这通常不是预期行为。总结一份够用且诚实的浮点规范RFC 3514 的核心成就在于为 Rust 浮点行为画出了一条清晰的边界线绝大多数代码遵循 IEEE 754-2008NaN 位模式在少数可列举的观察点上非确定编译期与运行期之间不存在 NaN 位的一致性保证内联汇编不得破坏浮点环境。它既没有承诺硬件级的完全确定性那会扼杀编译器优化也没有放弃对信号 NaN、首选 NaN 等关键性质的保护那会破坏pow等库函数的可依赖行为。对 Rust 开发者而言最实用的三条守则是不观察 NaN 的位避免to_bits/from_bits等操作你的浮点代码就是严格 IEEE 行为若要观察 NaN 位请接受其结果随版本、目标与运行次数变化的事实并把const中的 NaN 位模式视为某个合法选择但不必与运行时一致不要依赖浮点异常状态或 trap-on-NaN 调试技术所隐含的假设也不要让内联汇编改动浮点控制位。完整的规范原文与全部论证细节可查阅仓库中的 text/3514-float-semantics.md其配套的浮点类型扩展text/3453-f16-and-f128.md、IEEE 754 方法补充text/3173-float-next-up-down.md与const fn基础机制text/0911-const-fn.md等 RFC 也可在 text/ 目录下继续深入研读。赞分享文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载相关推荐Pony 0.63.0 版本解读IOCP 内存安全修复、IEEE 754 浮点语义对齐与 PonyTest 测试随机化Pony 0.63.0 版本解读IOCP 内存安全修复、IEEE 754 浮点语义对齐与 PonyTest 测试随机化 本篇技术指南围绕 Pony 编程语言编程语言编译器语言运行时Gopeed 完整指南一个免费下载管理器搞定 5 种协议还能让 AI 替你管任务Gopeed 完整指南一个免费下载管理器搞定 5 种协议还能让 AI 替你管任务 GopeedGo Speed够快下载器是一个用 Go 写内核、Flu桌面应用移动开发CLI前端后端PyO3 FFI 新增 PyFloat_Pack/Unpack{2,4,8}非受限 API 下的 IEEE 754 浮点二进制打包与解包PyO3 FFI 新增 PyFloat_Pack/Unpack{2,4,8}非受限 API 下的 IEEE 754 浮点二进制打包与解包 本篇围绕 PyO3开发工具上一篇PEFT 参数高效微调方法全景指南Prompt 提示、Layer Tuning 与 Adapter 适配器方法下一篇flutter_flutter 仓库在 Android Studio 中配置 Kotlin 格式化ktlint 插件与 CI 规则对齐指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?