Rust 比 C 更快吗?
最近有人在 Reddit 上提问:
在所有条件相同的情况下,是什么让 Rust 的实现比 C 的实现更快?
我认为这是一个非常有趣的问题!这个问题很难回答,因为它最终取决于你所说的“所有条件相同”到底是什么意思。我认为这是比较语言时遇到的一个难题。
以下是一些你可以认为“相同”但又“不同”的情况,以及它们对运行时性能的影响。
内联汇编
Rust 语言内置了内联汇编。C 语言将内联汇编作为一种非常常见的编译器扩展,以至于说它不是语言的一部分都显得有些吹毛求疵了。
以下是 Rust 中的一个示例:
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)
}
这段代码使用 rdtsc
读取时间戳计数器,并返回其值。
以下是 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 中,这两段代码生成的汇编代码相同:
rdtsc:
rdtsc
shl rdx, 32
mov eax, eax
or rax, rdx
ret
以下是 Godbolt 上的链接:https://godbolt.org/z/f7K8cfnx7
这算吗?我不知道。我认为这并不能真正回答这个问题,但这是回答这个问题的一种方式。
相似的代码,不同的结果
Rust 和 C 对相似的代码可能有不同的语义。以下是 Rust 中的一个结构:
struct Rust {
x: u32,
y: u64,
z: u32,
}
以下是 C 中的“相同”结构:
struct C {
uint32_t x;
uint64_t y;
uint32_t z;
};
在 Rust 中,该结构为 16 字节(同样是在 x86_64 上),而在 C 中为 24 字节。这是因为 Rust 可以自由地重新排序字段以优化大小,而 C 则不能。
这是相同还是不同?
在 C 中,您可以重新排序字段以获得相同的大小。在 Rust 中,你可以写 #[repr(C)]
来获得与 C 相同的布局。这是否意味着我们应该编写不同的 Rust 或不同的 C 来获得“相同”的东西?
社会因素
有些人报告说,由于 Rust 的检查,他们更愿意编写比同等 C(或 C++)更危险一些的代码,而在 C(或 C++)中,他们会进行更多的复制以确保安全。这在“同一开发团队在同一项目中”的意义上是“相同的”,但代码会因判断差异而不同。你可以认为这并非相同,而是不同。
一个很久以前的例子是 Stylo 项目。Mozilla 曾两次尝试用 C++ 并行化 Firefox 的样式布局,但两次都失败了。多线程太难搞定了。第三次,他们使用了 Rust,终于成功了。这是同一个组织做的同一个项目(虽然我认为不是同一个程序员),但一个成功了,一个失败了。这是“相同”的吗?从某些角度来说是,但从其他角度来说不是。
这同样适用于一个类似的问题:假设我们有一个初级开发人员在写 Rust,也在写 C,做的是同一个任务。我们会在其中一种语言中获得更快的代码吗?这控制了能力,但控制不了相同的代码。这是“相同”的吗?我不知道。那么,如果每个语言都有一个专家,一个非常了解 Rust 但不懂 C 的人,或者相反,他们被赋予了相同的任务,情况会如何呢?这与初级或“普通”开发人员不同吗?
编译时间与运行时间?
另一个 Reddit 用户问道:
我不是 Rust 专家,但大多数(所有?)安全检查都是编译时检查吗?它们不应该对运行时产生任何影响。
这是一个很好的问题!部分原因是默认值的不同。
array[0]
在两种语言中都是有效的。
在 Rust 中,运行时会进行边界检查。在 C 中,则不会。这是否意味着它们是相同的?在 Rust 中,我可以编写 array.get_unchecked(0)
,并获得 C 的语义。在 C 中,我可以编写边界检查来获得 Rust 的语义。这些是“相同的”吗?
在 Rust 中,如果编译器能够证明它是安全的,则该检查可能会被优化掉。在 C 中,如果我们手动编写了边界检查,那么如果编译器能够证明它是安全的,则该检查可能会被优化掉。它们是否“相同”?
他们说 Rust 的许多安全检查是在编译时进行的,这并没有错。但有些是在运行时进行的。但这又引出了另一个有趣的问题:编译时检查可能会导致你为与 C 相同的任务编写不同的代码。一个常见的例子是使用索引而不是指针。这可能意味着生成的代码性能不同。该检查真的是“在编译时”进行的吗?从技术上讲,在微观层面上,是的。在工程层面上呢?可能不是!
我的结论
我认为这个问题最重要的部分与可能性有关,即:
- 如果我们假设 C 是“最快的语言”,无论这意味着什么。
- Rust 无法做到同样的事情,是否有内在的原因?
我认为答案是“不”,即使不考虑内联汇编的情况。因此,在最关键、最根本的层面上,答案是“两者没有区别”。
但我们通常不是在讨论这个。我们通常是在工程背景下讨论,涉及特定项目、特定开发人员、特定时间限制等等。
我认为存在太多变量,因此难以得出普遍适用的结论。