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 相同的任务编写不同的代码。一个常见的例子是使用索引而不是指针。这可能意味着生成的代码性能不同。该检查真的是“在编译时”进行的吗?从技术上讲,在微观层面上,是的。在工程层面上呢?可能不是!

我的结论

我认为这个问题最重要的部分与可能性有关,即:

  1. 如果我们假设 C 是“最快的语言”,无论这意味着什么。
  2. Rust 无法做到同样的事情,是否有内在的原因?

我认为答案是“不”,即使不考虑内联汇编的情况。因此,在最关键、最根本的层面上,答案是“两者没有区别”。

但我们通常不是在讨论这个。我们通常是在工程背景下讨论,涉及特定项目、特定开发人员、特定时间限制等等。

我认为存在太多变量,因此难以得出普遍适用的结论。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注