Rust 1.66.0 版本发布

2022 年 12 月 15 日 · Rust 发布团队

Rust 团队很高兴地宣布 Rust 新版本 1.66.0 的发布。Rust 是一种编程语言,旨在赋予每个人构建可靠且高效软件的能力。

如果您之前通过 rustup 安装了 Rust,您可以使用以下命令获取 1.66.0 版本:

$ rustup update stable

如果您还没有安装,您可以从我们网站的相应页面获取 rustup,并查看 GitHub 上 1.66.0 版本的详细发布说明

如果您想通过测试未来版本来帮助我们,您可以考虑在本地更新以使用 beta 频道 (rustup default beta) 或 nightly 频道 (rustup default nightly)。请报告您可能遇到的任何错误!

1.66.0 稳定版的新特性

带字段枚举的显式判别值

具有整数表示的枚举现在可以使用显式判别值,即使它们带有字段。

#[repr(u8)]
enum Foo {
    A(u8),
    B(i8),
    C(bool) = 42,
}

以前,您可以在具有表示的枚举上使用显式判别值,但前提是它们的变体都没有字段。当跨语言边界传递值时,显式判别值非常有用,在这些情况下,枚举的表示形式需要在两种语言中匹配。例如:

#[repr(u8)]
enum Bar {
    A,
    B,
    C = 42,
    D,
}

这里,Bar 枚举保证具有与 u8 相同的布局。此外,Bar::C 变体保证具有 42 的判别值。未明确指定值的变体将根据它们在源代码中的顺序自动分配判别值,因此 Bar::A 将具有 0 的判别值,Bar::B 将具有 1 的判别值,而 Bar::D 将具有 43 的判别值。如果没有此功能,设置 Bar::C 的显式值的唯一方法是在它之前添加 41 个不必要的变体!

注意:虽然对于无字段枚举,可以通过 as 转换(例如 Bar::C as u8)来检查判别值,但 Rust 没有提供任何语言级别的方法来访问带有字段的枚举的原始判别值。相反,目前必须使用不安全代码来检查带有字段的枚举的判别值。由于此功能旨在用于跨语言 FFI,而 FFI 本身已经需要不安全代码,因此这应该不会带来太大的额外负担。同时,如果您只需要判别值的不透明句柄,请参阅 std::mem::discriminant 函数。

core::hint::black_box

在进行基准测试或检查编译器生成的机器代码时,通常需要阻止在某些地方发生优化。在以下示例中,函数 push_cap 在循环中执行 Vec::push 4 次:

fn push_cap(v: &mut Vec<i32>) {
    for i in 0..4 {
        v.push(i);
    }
}

pub fn bench_push() -> Duration { 
    let mut v = Vec::with_capacity(4);
    let now = Instant::now();
    push_cap(&mut v);
    now.elapsed()
}

如果您检查 x86_64 上编译器优化后的输出,您会注意到它看起来相当短:

example::bench_push:
  sub rsp, 24
  call qword ptr [rip + std::time::Instant::now@GOTPCREL]
  lea rdi, [rsp + 8]
  mov qword ptr [rsp + 8], rax
  mov dword ptr [rsp + 16], edx
  call qword ptr [rip + std::time::Instant::elapsed@GOTPCREL]
  add rsp, 24
  ret

实际上,我们想要进行基准测试的整个函数 push_cap 都被优化掉了!

我们可以使用新稳定的 black_box 函数来解决这个问题。从功能上讲,black_box 并没有什么特别之处:它接受您传递给它的值,然后将其原样返回。然而,在内部,编译器将 black_box 视为一个可以对其输入执行任何操作并返回任何值的函数(顾名思义)。

这对于禁用像我们上面看到的那种优化非常有用。例如,我们可以向编译器暗示,在 for 循环的每次迭代之后,实际上都会使用该向量来做一些事情。

use std::hint::black_box;

fn push_cap(v: &mut Vec<i32>) {
    for i in 0..4 {
        v.push(i);
        black_box(v.as_ptr());
    }
}

现在我们可以在我们的优化后的汇编输出中找到展开的 for 循环:

  mov dword ptr [rbx], 0
  mov qword ptr [rsp + 8], rbx
  mov dword ptr [rbx + 4], 1
  mov qword ptr [rsp + 8], rbx
  mov dword ptr [rbx + 8], 2
  mov qword ptr [rsp + 8], rbx
  mov dword ptr [rbx + 12], 3
  mov qword ptr [rsp + 8], rbx

您还可以在此汇编输出中看到调用 black_box 的副作用。指令 mov qword ptr [rsp + 8], rbx 在每次迭代后都会无用地重复。此指令将地址 v.as_ptr() 写入为函数的第一个参数,但该函数实际上从未被调用。

请注意,生成的代码根本不关心 push 调用可能引入的分配。这是因为编译器仍然使用我们在 bench_push 函数中调用 Vec::with_capacity(4) 的事实。您可以尝试调整 black_box 的位置,或尝试在多个位置使用它,以查看其对编译器优化的影响。

cargo remove

在 Rust 1.62.0 中,我们引入了 cargo add,这是一个用于向您的项目添加依赖项的命令行实用程序。现在您可以使用 cargo remove 来删除依赖项。

稳定的 API

其他更改

Rust 1.66 版本中还有其他更改,包括:

  • 现在你可以在模式中使用 ..=X 范围。
  • Linux 构建现在分别使用 LTO 和 BOLT 优化 rustc 前端和 LLVM 后端,从而提高运行时性能和内存使用率。

查看 RustCargoClippy 中的所有更改。

1.66.0 版本的贡献者

许多人共同努力创建了 Rust 1.66.0。没有你们所有人,我们不可能做到。谢谢!