这是前段时间在 HackerNews 上刷到的一篇热帖。博主 Steve Klabnik 从多个维度进行了解析,核心观点是:Rust 与 C 在性能上没有本质差异,实际表现取决于工程实现与具体场景。
Reddit 上最近有人提出了一个很有意思的问题:
在所有条件都相同的情况下,什么因素会让一个函数的 Rust 实现比 C 实现运行得更快?
这个问题很棒,但回答起来很有挑战性,因为最终的答案取决于你对“所有条件都相同”这个前提的具体定义,而这恰恰是不同编程语言之间难以直接比较的关键所在。下面,我们通过几个具体方面来探讨,看看哪些看似相同的条件,实则存在差异,并分析这些差异如何影响运行时性能。
内联汇编的影响
Rust 语言原生支持内联汇编,而 C语言 的内联汇编虽是一种极为常用的编译器扩展,但严格来说并不属于语言标准。
来看一个 Rust 读取时间戳计数器 (TSC) 的例子:
use std::arch::asm;
#[unsafe(no_mangle)]
pub fn rdtsc() -> u64 {
let lo: u32;
let hi: u32;
unsafe {
asm!(
"rdtsc",
out("eax") lo,
out("edx") hi,
options(nomem, nostack, preserves_flags),
);
}
((hi as u64) << 32) | (lo as u64)
}
对应的 C 语言实现如下:
#include <stdint.h>
uint64_t rdtsc(void)
{
uint32_t lo, hi;
__asm__ __volatile__ (
"rdtsc"
: "=a"(lo), "=d"(hi)
);
return ((uint64_t)hi << 32) | lo;
}
当使用 rustc 1.87.0 和 clang 20.1.0 编译器并开启 -O 优化时,两者会生成完全相同的汇编代码:
rdtsc:
rdtsc
shl rdx, 32
mov eax, eax
or rax, rdx
ret
你可以在 Godbolt 上查看完整的对比:https://godbolt.org/z/f7K8cfnx7
这种情况算是 Rust 比 C 快吗?严格来说,这只是展示了在特定能力(内联汇编)上,两者可以做到完全对等,并没有直接回答问题本身,但它为我们提供了一个关键的思考起点。
语义差异导致的不同结果
即使写出了表面相似的代码,Rust 和 C 也可能因为语言语义的不同而产生不同的结果。例如,我们定义一个结构体。
在 Rust 中:
struct Rust {
x: u32,
y: u64,
z: u32,
}
在 C 语言中:
struct C {
uint32_t x;
uint64_t y;
uint32_t z;
};
在 x86_64 架构下,这个 Rust 结构体的大小是 16 字节,而 C 结构体的大小是 24 字节。原因在于 Rust 编译器可以自由重排结构体字段以优化内存占用,而 C 语言则遵循严格的声明顺序。
这算条件相同吗?在 C 中,你可以手动调整字段顺序来达到相同的内存布局;在 Rust 中,你也可以通过 #[repr(C)] 属性来强制使用与 C 兼容的布局。那么,为了实现“所有条件相同”,我们是否本就该写出不同的 Rust 或 C 代码呢?
开发环境与开发者信心的作用
有开发者分享过一个经验:得益于 Rust 强大的编译期和运行时安全检查,他们更愿意在 Rust 项目中尝试一些有潜在风险的高性能优化手段。相反,在使用 C 或 C++ 时,出于对内存安全等问题的担忧,他们往往会选择更保守、更安全的实现方式,例如增加一次不必要的数据拷贝。
这种情况看似是“同一批开发者做同一个项目”,但最终产物却因语言特性带来的心理预期和风险判断不同而产生了差异。你可以说这不算“条件相同”,但也完全可以认为根本的开发条件和人员是一致的。
一个著名的例子是 Mozilla 的 Stylo 项目。Mozilla 曾两次尝试用 C++ 实现 Firefox 样式系统的并行化,均因多线程编程的复杂性而失败。第三次改用 Rust 后,项目最终成功落地并发布。这是同一家公司、同一个目标的项目,却因编程语言的选择不同而得到了截然不同的结果。这算条件相同吗?从某些角度看是,从另一些角度看则不是。
由此延伸出更多思考:如果让一名初级开发者分别用 Rust 和 C 完成同一项任务,哪种语言能产出性能更好的代码?这虽然控制了“开发者”这个变量,但无法保证代码逻辑完全一致,算条件相同吗?更进一步,如果让各自领域的专家(精通 Rust 但不懂 C,以及精通 C 但不懂 Rust)来竞争,结果又会怎样?这与普通开发者的情况又有何不同?这些问题都让“控制变量”变得异常困难。
编译期检查与运行时开销
另一位 Reddit 网友提出了一个关键点:
我不是 Rust 专家,但 Rust 的大部分(甚至所有)安全检查不都是在编译期完成的吗?理论上这应该不会影响运行时性能。
这是一个非常好的观察!但这其实又回到了编程语言默认规则的差异上。
对于 array[0] 这样的数组访问,在两种语言中都是合法的。但 Rust 默认会在运行时进行数组越界检查,而 C 语言则不会。这能算条件相同吗?在 Rust 中,你可以使用 array.get_unchecked(0) 来跳过检查,达到与 C 相同的语义;在 C 中,你也可以手动编写边界检查代码来模拟 Rust 的安全行为。那么,这些情况算不算“所有条件都相同”呢?
而且,在 Rust 中,如果编译器能够通过静态分析证明这次数组访问是安全的,它会自动优化掉运行时的检查代码。同样,在 C 中手动编写的检查,如果被编译器证明是冗余的,也会被优化掉。这些由编译器优化能力带来的差异,又该如何界定?
网友的说法没错,Rust 的许多安全检查确实在编译期完成,但仍有一部分(如上述的数组边界检查)留到了运行时。然而,更有趣的影响在于:编译期的安全检查,可能会促使开发者采用与 C 语言完全不同的代码结构和模式来完成相同的任务。
例如,Rust 的所有权和借用规则可能让开发者更倾向于使用索引迭代而非指针算术,这最终会导致生成的机器码在性能特征上有所不同。那么,这种编译期检查真的只是“编译期”的事吗?从纯粹的机器码层面看,答案是肯定的;但从工程实践和代码编写的角度看,它深刻地影响了开发者的思维模式和实现路径,答案可能是否定的。
结论与思考
归根结底,我认为这个问题的核心在于探讨“可能性”,可以分解为两个子问题:
- 如果我们承认 C 语言是“最快的系统编程语言”(无论这个“最快”如何定义)。
- Rust 是否存在某种根本性的、无法逾越的缺陷,导致它永远无法达到与 C 同等的性能水平?
对于第二个问题,我的答案是否定的。即使不考虑内联汇编这种对等能力,Rust 在设计上也没有本质性的性能缺陷。因此,在最根本、最理论化的层面上,Rust 和 C 在性能上没有本质区别,两者都能产生同等高效的机器码。
但是,现实中的软件开发远比理论复杂。我们面对的是具体的项目需求、特定水平的开发团队、有限的时间预算以及复杂的交付环境。在这些具体的、充满约束的现实条件下,很难对 Rust 和 C 的性能做出一个放之四海而皆准的结论。语言的安全性、工具链的成熟度、团队的经验、生态库的质量等,都会成为影响最终性能表现的关键变量。
技术的选择与讨论,最终需要回归到具体的应用场景中。如果你对系统级编程和性能优化有更多兴趣,欢迎到 云栈社区 的相关板块与更多开发者交流探讨。
(本文基于 Steve Klabnik 的文章 “Is Rust Faster Than C?” 编译并优化,原文地址:https://steveklabnik.com/writing/is-rust-faster-than-c)