发布 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 channel (rustup default beta) 或 nightly channel (rustup default nightly)。请报告您遇到的任何错误!

1.66.0 stable 版本中的新特性

带有字段的枚举的显式判别值

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

#[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 没有提供语言层面的方法来访问带字段枚举的原始判别值。目前,必须使用 unsafe 代码来检查带字段枚举的判别值。由于此特性旨在用于跨语言 FFI,而 FFI 中已经需要 unsafe 代码,因此这应该不会增加太多额外负担。同时,如果您只需要一个判别值的 opaque handle,请参考 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。没有你们所有人,我们不可能做到。 感谢大家!