在数值计算圈子里,Rust 长期背着一个不太光彩的标签:同一段点积代码,同样的硬件,开启优化后性能却显著落后于 C++。问题不在代码生成器本身,而在语言对浮点语义的严格承诺。Rust 1.98 的发布,正是冲着这块短板来的。
Rust 1.98 更新了什么
据 IT之家援引 Linuxiac 的报道,Rust 团队推出 1.98 版本,核心变化是为 f32 与 f64 浮点类型引入一组“代数浮点”(algebraic floating-point)方法,包括 algebraic_add、algebraic_sub、algebraic_mul、algebraic_div 与 algebraic_rem 五个新 API。同版本还加快了整数格式化速度,并稳定了多项标准库接口。
这组方法要解决的是 IEEE 754 标准的历史包袱。浮点数用有限位数近似实数,每一步运算都伴随舍入,结果是加法不再严格满足结合律——(a + b) + c 与 a + (b + c) 在特定取值下会给出不同结果。编译器为了不悄悄改变程序行为,禁止对浮点运算重新排序;而 SIMD 向量化、多累加器展开这些关键优化恰恰依赖重排。报道称,这条铁律曾让 Rust 在简单点积基准上的速度只有 C++ 的八分之一,因为 C++ 一侧通常开着允许结合重排的 fast-math 类选项。
新方法的含义,是开发者以单个表达式为单位向编译器“签字放行”:这段运算可以按实数的代数性质处理,结合律、分配律都可以用。放行范围之外的浮点代码,仍然保持严格的 IEEE 754 语义,一位不差。
为什么不用编译开关
C/C++ 的传统答案是 -ffast-math:一个全局开关,打开后整个编译单元的浮点语义一起放宽。性能确实上去了,代价是 NaN 传播、无穷大处理、舍入模式都可能改变,而且改动发生在编译期,源码里不留痕迹。数值 bug 一旦混入,定位起来往往像大海捞针。
Rust 的选择是把粒度做细:默认严格,逐处放宽,每处放宽都写在源码里。code review 时,哪些运算牺牲了精确性、哪些保持标准语义,一目了然。这与 Rust 一贯的显式哲学一脉相承——unsafe 如此,代数浮点方法也如此。
影响与点评
对机器学习推理、图形学、科学计算这类浮点密集场景,这是一次实打实的能力补齐。此前想在 Rust 里写出对 SIMD 友好的浮点归约,要么手写 unsafe 内联汇编,要么绕道第三方库,如今多了一条标准库内的正路。编译器拿到了重排许可证,后续在自动向量化上还有持续挖掘的空间。
值得肯定的是设计取向:Rust 没有沿用“全局开关换性能”的老方案,而是把精度与速度的权衡变成源码中可见、可审计的局部决策。代价是 API 稍显啰嗦——五个方法替换五个运算符——但换来的是数值 bug 可定位、可归因。对构建大型可维护系统的团队来说,这份显式性物有所值。
点积追平 C++ 只是第一站。如果生态里的数值计算库陆续接入这组方法,Rust 在科学计算版图上的位置将实质性前移;而“逐点放行”这个思路,也给其他坚持严格语义的语言提供了一个可借鉴的样本。