配了三年 Rust 数值计算,每次跑矩阵乘法都比 C++ 慢八倍——今天代数运算把这件事彻底修了
配了三年 Rust 数值计算,每次跑矩阵乘法都比 C++ 慢八倍——今天代数运算把这件事彻底修了
同样一段点积代码,Rust 跑出来比 C++ 慢 8 倍,不是 LLVM 生成的汇编质量不行,是 IEEE 754 的浮点数加法根本不满足结合律,编译器没权利动它。这是 2025 年一个 GitHub issue 的发现,Rust 团队今天用 1.98 的代数浮点运算从标准库里给出了答案。
为什么 Rust 数值计算比 C++ 慢这么多
问题的根子出在 IEEE 754 标准上。浮点数的加法不满足结合律——(a + b) + c 不等于 a + (b + c)。编译器为了保证「结果和源代码完全一致」,只能保守地逐项累加,循环向量化(SIMD)这条路就被堵死了。
实际影响有多大?有人在 Intel i7-12700K 上测过,1 百万个 f64 值的加法,普通写法 595 微秒;换用代数运算方法,配合 256 位 SIMD 指令,同样硬件只需要 156 微秒。差了将近 4 倍。而 C++ 在相同场景下能跑出这个速度,是因为主流 C++ 编译器默认就开了 -ffast-math,编译器被允许「按数学规律办事」。
algebraic_* 五件套:把许可下放给编译器
Rust 1.98 给 f32 和 f64 各新增了 5 个代数方法:
// algebraic_add: 加法允许编译器重排
let sum: f64 = values.iter()
.copied()
.fold(0.0, |acc, x| acc.algebraic_add(x));
// pairwise summation 示例(与 NumPy 同算法)
fn pairwise_sum(slice: &[f64]) -> f64 {
let n = slice.len();
if n < 4 {
slice.iter().copied().fold(0.0, f64::algebraic_add)
} else {
let mid = n / 2;
let left = pairwise_sum(&slice[..mid]);
let right = pairwise_sum(&slice[mid..]);
left.algebraic_add(right)
}
}
这五个方法本质上是给编译器的一张许可证:这段代码我允许你按实数的代数性质重新排序运算,代价是结果可能因编译器选择不同而略有差异,但绝不会产生未定义行为(UB)。algebraic_add、algebraic_sub、algebraic_mul、algebraic_div、algebraic_rem,分别对应加、减、乘、除、取模。
format_into:把 itoa 从依赖里删掉
1.98 还稳定了一个很多人等了多年的功能——整数格式化跳过动态分发。所有原生整数类型(i8~i128、u8~u128、isize、usize)都新增了 format_into 方法:
use std::fmt::NumBuffer;
let mut buf = NumBuffer::new();
let s = 42_u32.format_into(&mut buf);
// 不再依赖第三方 crate,性能和 itoa 持平
write! 宏内部走的动态分发路线被跳过了,直接往预分配缓冲区写字符,benchmark 数据和 itoa crate 相当。以前为了格式化一个整数加一行 itoa = “1.0” 的项目,现在可以直接删掉这个依赖。
ManuallyDrop 稳定性保证
1.98 之前,移动一个内部 Box 已被 drop 的 ManuallyDrop<Box<T>> 是 UB。1.96.0 修了这个问题,但没写进文档,1.98 把它补上了——这个行为现在是稳定的,未来版本不会变。
怎么用起来
// 第一步:升级 Rust
rustup update stable
rustc --version // 1.98.0
// 第二步:代数运算提速数值代码
// 旧代码
let sum: f64 = data.iter().fold(0.0, |a, &b| a + b);
// 新代码
let sum: f64 = data.iter().fold(0.0, |a, &b| a.algebraic_add(b));
// 第三步:去掉 itoa 依赖
// Cargo.toml 删掉 itoa = "1.0"
// 代码里改成
use std::fmt::NumBuffer;
let mut buf = NumBuffer::new();
let s = value.format_into(&mut buf);
目前 algebraic_* 方法在 x86_64-v3 及以上 target 能发挥最大效果,配合 RUSTFLAGS=”-C target-cpu=x86-64-v3″ 编译时,编译器会发射 AVX2 SIMD 指令,256 位一次处理 4 个 f64 或 8 个 f32。没有这个 target 的硬件,编译器也会尽量优化,不需要写任何 SIMD intrinsic 代码。
适合谁用
这次改动最直接受益的是三类场景:机器学习推理(矩阵乘法、注意力计算)、信号处理(滤波器、FFT)、游戏引擎物理计算。这些场景以前要么靠 unsafe 的 std::arch,要么直接依赖 C/C++ 库,现在可以在纯 Rust 里用标准库实现同等性能了。
三年 Rust 用下来,每次和 C++ 比数值计算都矮一截,这件事今天被 algebraic 运算从标准库里修了。不用加依赖,不用写 unsafe,打开 Cargo.toml 确认 Rust 版本到 1.98,剩下就是改几个方法名的事。
评论区
登录后可评论。